Promises

JavaScript · Practical · 15 min

Every modern async API — fetch, file system, databases — returns promises.

A promise is an object standing for a value that will be available later. It is in one of three states: pending, then either fulfilled with a value or rejected with a reason (usually an Error). Once settled, it never changes.

You react with methods that each return a new promise, which is what makes chaining work:

  • .then(onFulfilled) — receives the value. Whatever you return becomes the next promise's value; if you return a promise, the chain waits for it.
  • .catch(onRejected) — handles a rejection from any earlier step. Throwing inside a then rejects the chain too.
  • .finally(fn) — runs either way, for cleanup.
loadUser(id)
  .then(user => loadOrders(user.id))
  .then(orders => render(orders))
  .catch(showError);

Always return from then callbacks when you start more async work; forgetting return breaks the chain and the next step runs too early. Always end a chain with catch (or await it in a try): an unhandled rejection is reported as an error, and Node.js exits by default.

Combinators run promises concurrently:

  • Promise.all(ps) — fulfils with an array of all values; rejects as soon as any rejects.
  • Promise.allSettled(ps) — waits for all, giving { status, value | reason } for each.
  • Promise.race(ps) — settles like the first one to settle.
  • Promise.any(ps) — the first to fulfil; rejects only if all reject.

To wrap a callback API, use the constructor: new Promise((resolve, reject) => ...). Promise.withResolvers() (ES2024) returns { promise, resolve, reject } when you need to resolve from outside. Promise.resolve(x) and Promise.reject(err) create already-settled promises.

Example

function delay(ms, value) {
  return new Promise(resolve => setTimeout(() => resolve(value), ms));
}
function failAfter(ms) {
  return new Promise((_, reject) => setTimeout(() => reject(new Error("boom")), ms));
}

delay(20, 2)
  .then(n => n * 10)                    // plain value
  .then(n => delay(20, n + 1))          // returned promise is awaited
  .then(n => console.log("chain:", n))  // chain: 21
  .catch(err => console.error(err))
  .finally(() => console.log("chain done"));

Promise.all([delay(10, "a"), delay(30, "b")])
  .then(values => console.log("all:", values));  // all: [ 'a', 'b' ]

Promise.allSettled([delay(10, "ok"), failAfter(5)])
  .then(results => console.log(results.map(r => r.status))); // [ 'fulfilled', 'rejected' ]

Promise.any([failAfter(5), delay(15, "first success")])
  .then(v => console.log("any:", v));    // any: first success

Remember

  • A promise is pending, then fulfilled or rejected — once
  • then returns a new promise; return values (or promises) to chain
  • all fails fast, allSettled waits for everything, any wants one success

Watch out for

  • Forgetting to return a promise inside .then, so the next step doesn't wait.
  • Leaving a chain without .catch, producing unhandled rejections.

Check yourself

  1. You need results from three requests and want the others even if one fails. Which combinator fits?
  2. Inside .then(x => { fetchMore(x); }), fetchMore returns a promise. What does the next .then receive?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises