Coderrob brand logo Coderrob

Hi, I'm Rob—programmer, Pluralsight author, software architect, emerging technologist, and lifelong learner.


Javascript Promise


Wed, 22 Oct 2025

A Promise is a guaranteed placeholder value for what a function or series of functions will return.

It’s a promise to return a value eventually.

The function swears this time is different. You can trust it. It swears it’ll get it back to you in no time.

If you haven’t already, check out my post on JavaScript Callbacks where we dove into callback hell and why we desperately needed something better. Spoiler: Promises are that something better.

Remember those callback signatures? There’s almost always an error parameter and a data parameter. Success and/or failure, wrapped up in that classic pattern:

function callback(err, data) {
}

The Promise takes this concept and formalizes it. Instead of passing callbacks around, we get rejections (errors) or resolutions (data) returned as promise states.

When creating a new Promise, you can see in the constructor that you’re able to explicitly control either outcome:

const op = new Promise((resolve, reject) => {
})

The resolve and reject are functions used to signal success or failure.

Most of the time you won’t be creating promises directly - you’ll be using them from APIs that already return promises. But there will come a day when you need to wrap some old callback-based code into a shiny new promise. Let’s look at how to do that.

Remember our callback hell example from before? Let’s see how we’d convert one of those levels into a promise:

// The old callback way
function moveLevel(name, callback) {
    setTimeout(() => {
        const success = Math.random() > 0.2; // 80% success rate
        if (success) {
            callback(null, `Made it through ${name}`);
        } else {
            callback(new Error(`Failed at ${name}`), null);
        }
    }, 100);
}

// The promise way
function moveLevelAsync(name) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            const success = Math.random() > 0.2;
            if (success) {
                resolve(`Made it through ${name}`);
            } else {
                reject(new Error(`Failed at ${name}`));
            }
        }, 100);
    });
}

See the difference? No more callback parameter. We just resolve or reject right there in the promise.

Promise Resolutions

Resolving a promise can be done in two ways. Resolving a value immediately using the Promise object, or resolving from inside a Promise.

Resolving a Promise Directly

Using the Promise object to return a value directly without creating a new Promise object.

Promise.resolve()

Calling the resolve() will return a thenable response but without passing a value.

Promise.resolve().then((data) => {
    console.log(data);
})
>_ undefined

Passing data can be done by passing a single argument value to the resolve function.

Promise.resolve(true).then((data) => {
    console.log(data);
})
>_ true

This just doesn’t work…

Promise.resolve(true, true).then((data1, data2) => {
    console.log(data1, data2);
>_ true undefined

You’re able to resolve just one value.

If you need more than one value you’ll want to return an array, or object with the necessary property information.

You can return another promise if you’re so inclined.

Resolving Inside a Promise

Here’s a more practical example of resolving inside a promise:

const op = new Promise((resolve, reject) => {
    const data = longRunningProcess();
    if (data) {
        return resolve(data);
    }
    // If we got here, something went wrong
    reject(new Error('Process returned nothing'));
})

That’s it. Check if you got data, resolve if you did, reject if you didn’t. Simple.

Promise Rejections

A Promise can fail just as easily as it can succeed. Use Promise.reject() to create an immediately rejected promise, or call the reject() function inside the constructor when something goes wrong.

// Immediate rejection. It's not you, it's me.
Promise.reject(new Error('Something went sideways'))
  .catch(err => console.error('Rejected:', err.message));

// Rejection inside a promise. The digital "Dear John" letter.
const op = new Promise((resolve, reject) => {
  const data = maybeBrokenProcess();
  if (!data) {
    return reject(new Error('No data returned'));
  }
  resolve(data);
});

op.catch(err => console.log('Handled:', err.message));

A rejected promise propagates down the chain (like our feelings) until a catch handles it. If you forget to handle rejections, today’s runtimes will usually warn you. Thankfully.

TIP: When running unit tests ensure to enable leak detection when working with promises.

PS: Jokes aside, it’s rough out there and if you are feeling depressed, thinking about suicide, or worried about a friend or loved one. Please reach out right away:

U.S. Suicide & Crisis Lifeline: Dial 988

Promise Chains

Promises shine when you chain them. Each .then() returns a new promise, allowing you to transform values step-by-step.

fetchUser()
  .then(user => fetchOrders(user.id))
  .then(orders => calculateTotals(orders))
  .then(totals => console.log('Totals:', totals))
  .catch(err => console.error('Chain error:', err.message));

And then, and then, and then. Only scene that is rememberable from the film Dude Where’s My Car

Important rules:

Example: returning vs not returning

Promise.resolve(1)
  .then(val => val + 1)            // returns 2
  .then(val => Promise.resolve(val + 2)) // returns promise that resolves to 4
  .then(val => console.log(val)); // 4

If you forget to return, the next .then() receives undefined.

Parallel vs. Race: Promise.all and Promise.race

Sometimes you want to run tasks in parallel and wait for them all, or you just want the first result.

const p1 = Promise.resolve('a');
const p2 = new Promise(r => setTimeout(() => r('b'), 100));

Promise.all([p1, p2]).then(results => console.log('all:', results)); // ['a','b']

Promise.race([p1, p2]).then(first => console.log('race:', first)); // 'a'

Notes:

Error Handling Strategies

There are a few ways to handle errors with promises. Pick what makes sense for your situation:

1. Centralized catch at the end (most common):

doWork()
  .then(step1)
  .then(step2)
  .catch(err => console.error('Fatal:', err.message));

Everything fails into that one catch. Clean and simple.

2. Handle a specific step’s error and keep going:

stepThatMayFail()
  .catch(err => {
    console.warn('Step failed, using fallback');
    return fallbackValue; // chain continues with fallback
  })
  .then(nextStep);

This is great when you have a Plan B and don’t want one failure to tank everything.

3. Use finally() for cleanup:

doSomething()
  .then(result => console.log('ok'))
  .catch(err => console.error('nope'))
  .finally(() => console.log('cleanup'));

The finally block runs whether you succeed or fail. Perfect for closing connections or cleaning up resources.

Async / Await: The Modern Way

async/await is syntactic sugar over promises that makes async code look synchronous. It’s usually easier to read and reason about:

async function getTotalCosts() {
  try {
    const user = await getUser();
    const orders = await fetchOrders(user.id);
    return calculateTotals(orders);
  } catch (err) {
    console.error('async error:', err);
    throw err; // rethrow if caller should handle it
  }
}

Look at that. No .then() chains. Just await the promise and keep going. If something fails, it throws into the catch block like synchronous code.

Want to run things in parallel with async/await? Use Promise.all:

async function parallel() {
  const [a, b] = await Promise.all([taskA(), taskB()]);
  return a + b;
}

Both tasks run at the same time, and you unpack the results. Fast and clean.

Promisify: Turning Old Callbacks into Promises

Sometimes you’re stuck with callback-based APIs (looking at you, Node.js fs module). You can wrap them into promises with a helper function:

function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (err, data) => {
        if (err) return reject(err);
        resolve(data);
      });
    });
  };
}

// Usage
const readFileAsync = promisify(fs.readFile);
readFileAsync('file.txt', 'utf8')
  .then(console.log)
  .catch(console.error);

Now that old callback API acts like a modern promise. Beautiful.

Note: Node.js has util.promisify() built-in now, so you don’t have to write this yourself anymore. But it’s good to understand how it works.

The Bottom Line

Promises are about clearly representing eventual values and errors. They turn callback hell into manageable chains (or even better, clean async/await code).

Key things to remember:

I promise that if you follow these patterns, your code will eventually resolve to something beautiful.

-Rob