Promises

JavaScript · Lesson 43 of 61 7 min read Updated

In the last lesson you saw how nested callbacks turn into "callback hell". A promise is the modern answer. It is an object that stands for a result that is not ready yet, like the token you get at a food court counter. You can hold on to it, pass it around, and say what should happen when the result arrives or when something goes wrong.

In this lesson you will use promises with then, catch and finally, create your own, chain several steps without nesting, and run tasks in parallel with Promise.all and friends. You will also meet Promise.withResolvers and the new Promise.try. Read How Asynchronous Code Works first.

The three states of a promise

A promise is always in one of three states:

  • Pending: the work is still going on.
  • Fulfilled: the work succeeded, and the promise holds a value.
  • Rejected: the work failed, and the promise holds a reason, usually an Error.

Once a promise is fulfilled or rejected, it is settled and can never change again. You react to it with three methods:

  • .then(callback) runs when the promise is fulfilled and receives the value.
  • .catch(callback) runs when it is rejected and receives the error.
  • .finally(callback) runs either way, which is useful for clean-up such as hiding a loading spinner.

Creating a promise

Most promises you use come from built-in functions such as fetch(). Sometimes you need to wrap older, callback-based code yourself. new Promise() takes a function, called the executor, which receives two functions: call resolve(value) on success and reject(error) on failure.

JavaScriptRun it yourself
function cookOrder(dish) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (dish === "Pizza") {
        reject(new Error("Sorry, pizza is not on the menu"));
      } else {
        resolve(`${dish} is ready`);
      }
    }, 500);
  });
}

const order = cookOrder("Masala dosa");
console.log(order instanceof Promise);

order
  .then((message) => console.log(message))
  .catch((error) => console.log(error.message))
  .finally(() => console.log("Token 1 closed"));

cookOrder("Pizza")
  .then((message) => console.log(message))
  .catch((error) => console.log(error.message))
  .finally(() => console.log("Token 2 closed"));
Output
true
Masala dosa is ready
Token 1 closed
Sorry, pizza is not on the menu
Token 2 closed

cookOrder returns the promise straight away, while the dosa is still "cooking". The callbacks run half a second later. Always reject with an Error object rather than a plain string, so the error has a message and a stack trace.

A very handy small helper you will see often is a promise-based pause: const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));.

Chaining instead of nesting

Here is the important part: .then() always returns a new promise. Whatever your callback returns becomes the value for the next .then(). If your callback returns another promise, the chain waits for it. That lets you write dependent steps as a flat list instead of a pyramid.

JavaScriptRun it yourself
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

function login(name) {
  return wait(300).then(() => ({ id: 7, name }));
}

function getOrders(userId) {
  return wait(300).then(() => [
    { id: 501, item: "Cricket bat" },
    { id: 502, item: "Kit bag" },
  ]);
}

function getDelivery(order) {
  return wait(300).then(() => {
    if (order.id === 502) {
      throw new Error(`No courier found for order ${order.id}`);
    }
    return `${order.item} arrives on Friday`;
  });
}

login("priya")
  .then((user) => getOrders(user.id))
  .then((orders) => getDelivery(orders[0]))
  .then((status) => console.log(status))
  .catch((error) => console.log(`Problem: ${error.message}`))
  .finally(() => console.log("Done"));
Output
Cricket bat arrives on Friday
Done

Compare this with the nested callback version from the previous lesson. Each step sits on its own line, and one .catch() at the end handles an error from any step. Change orders[0] to orders[1] and run it again: getDelivery throws, the remaining .then() is skipped, and the chain jumps straight to .catch(), printing "Problem: No courier found for order 502".

Running promises in parallel

Steps that do not depend on each other should run at the same time. Four static methods take an array of promises and return one combined promise:

MethodFulfils whenRejects when
Promise.all()All fulfil; gives an array of values in the original orderAny one rejects (it fails fast)
Promise.allSettled()All have settled; gives an array of { status, value } or { status, reason }Never
Promise.any()The first one fulfilsAll reject; gives an AggregateError
Promise.race()The first one to settle fulfilsThe first one to settle rejects

This example asks three shops for a price. One shop is closed:

JavaScriptRun it yourself
function checkShop(name, ms, price) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (price === null) {
        reject(new Error(`${name} is closed`));
      } else {
        resolve({ name, price });
      }
    }, ms);
  });
}

// A fresh set of three requests each time it is called
const askShops = () => [
  checkShop("Gupta Sports", 300, 1299),
  checkShop("Kochi Kits", 100, 1249),
  checkShop("Rohtak Traders", 200, null),
];

Promise.all(askShops())
  .then((results) => console.log(`all: ${results.length} prices`))
  .catch((error) => console.log(`all failed: ${error.message}`));

Promise.allSettled(askShops()).then((results) => {
  console.log(`allSettled: ${results.map((r) => r.status).join(", ")}`);
});

Promise.any(askShops()).then((first) => {
  console.log(`any: first price from ${first.name}`);
});

Promise.race(askShops()).then((first) => {
  console.log(`race: first answer from ${first.name}`);
});
Output
any: first price from Kochi Kits
race: first answer from Kochi Kits
all failed: Rohtak Traders is closed
allSettled: fulfilled, fulfilled, rejected

How to choose:

  • Use Promise.all() when you need every result and one failure makes the whole job useless.
  • Use Promise.allSettled() when you want every result you can get, such as a dashboard where one broken widget should not hide the others.
  • Use Promise.any() when any one success is enough, such as trying two mirror servers.
  • Use Promise.race() mainly to compete a task against a timer.

Promise.withResolvers

Sometimes the code that should resolve a promise lives outside the executor function, for example in an event listener or another object. Promise.withResolvers() (added in ES2024) returns the promise and its resolve and reject functions together, so you can hand them to whoever needs them.

JavaScriptRun it yourself
function createGate() {
  const { promise, resolve } = Promise.withResolvers();
  return { opened: promise, open: resolve };
}

const gate = createGate();
gate.opened.then((who) => console.log(`Gate opened by ${who}`));

console.log("Waiting at the gate...");
setTimeout(() => gate.open("the guard"), 500);
Output
Waiting at the gate...
Gate opened by the guard

Common mistakes

  • Forgetting to return inside .then(). Without return getOrders(id), the next step gets undefined and does not wait.
  • Nesting .then() inside .then(). Return the promise and chain instead.
  • No .catch() at the end. A rejection with no handler becomes an "unhandled rejection" error in the console.
  • Wrapping a promise in new Promise() for no reason. If a function already returns a promise, just return it or chain on it.
  • Running independent steps one after another. Start them together and use Promise.all().

Key points

  • A promise is pending, then settles once as fulfilled (with a value) or rejected (with an error).
  • then handles success, catch handles failure and finally runs either way.
  • .then() returns a new promise, so steps can be chained, and one .catch() covers the whole chain.
  • Promise.all, allSettled, any and race combine several promises running in parallel.
  • Promise.withResolvers() hands you resolve and reject outside the executor; Promise.try() turns any function into a promise.

Exercise

Write a function bookSeat(train) that returns a promise. After 400 ms it should resolve with "Seat booked on Rajdhani" when the train name is "Rajdhani" and reject with an Error for any other name. Then use Promise.allSettled() to book "Rajdhani" and "Ghost Express" at the same time, and log one line per result.

Show solution
JavaScript
function bookSeat(train) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (train === "Rajdhani") {
        resolve(`Seat booked on ${train}`);
      } else {
        reject(new Error(`${train} is not running today`));
      }
    }, 400);
  });
}

Promise.allSettled([bookSeat("Rajdhani"), bookSeat("Ghost Express")]).then((results) => {
  for (const result of results) {
    console.log(result.status === "fulfilled" ? result.value : result.reason.message);
  }
});

This prints "Seat booked on Rajdhani" and then "Ghost Express is not running today". allSettled never rejects, so you can inspect every result, successful or not.

Quick Quiz

1. Which state can a settled promise move to next?

2. What does .then() return?

3. Three requests run with Promise.all() and the second one rejects. What happens?

4. You want results from five widgets even if some fail. Which method fits?

Learn JavaScript with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call