</> HTML5Advent
ENFRESDEITPT

// js · Web Platform Advent #2

JavaScript Promises: a practical guide

Understand JavaScript promises — creating them, then/catch/finally, chaining, and combining many with Promise.all, race, any and allSettled — with runnable examples and the common gotchas.

A red jigsaw piece slotting into the last gap of a grey puzzle

A promise is an object that represents a value that isn't ready yet. It's the foundation of asynchronous JavaScript: fetch(), timers, file reads in Node, and most modern APIs return promises. A promise is always in one of three states — pending, fulfilled, or rejected — and once it settles it never changes again.

Creating a promise

Most of the time you consume promises that an API hands you. But you create one yourself with the Promise constructor, which takes an executor function receiving resolve and reject:

const wait = (ms) => new Promise((resolve) => {
  setTimeout(resolve, ms);
});

const fetchUser = (id) => new Promise((resolve, reject) => {
  if (!id) reject(new Error('id is required'));
  else resolve({ id, name: 'Ada' });
});

Calling resolve(value) fulfils the promise with that value; calling reject(error) rejects it. Anything thrown synchronously inside the executor also rejects the promise.

then, catch and finally

You read a promise's outcome with .then() for success, .catch() for failure, and .finally() for cleanup that runs either way:

fetchUser(42)
  .then((user) => console.log('Got', user.name))
  .catch((err) => console.error('Failed:', err.message))
  .finally(() => console.log('Done'));

.then() can take two arguments — onFulfilled and onRejected — but a separate .catch() at the end of the chain is clearer and also catches errors thrown inside earlier .then() handlers.

Chaining: the real superpower

Every .then() returns a new promise, so they chain. Whatever you return from a handler becomes the input to the next one. Crucially, if you return a promise, the chain waits for it to settle before continuing:

fetch('/api/user')
  .then((res) => res.json())          // returns a promise → chain waits
  .then((user) => fetch('/api/posts?user=' + user.id))
  .then((res) => res.json())
  .then((posts) => console.log(posts.length, 'posts'))
  .catch((err) => console.error(err));

A single .catch() at the end handles a rejection from any step above it, which is why chaining beats nesting callbacks.

Relay sprinters on a track passing the baton from one runner to the next
Like relay runners passing a baton, a promise chain hands each step's result to the next handler in sequence.

Running promises together

When tasks don't depend on each other, start them in parallel and combine the results. Four static methods cover the common cases:

  • Promise.all([...]) — waits for all to fulfil; rejects as soon as one rejects. Returns an array of results in order.
  • Promise.allSettled([...]) — waits for all to finish and never rejects; returns an array of { status, value } or { status, reason } objects.
  • Promise.race([...]) — settles with the first promise to settle, whether it fulfils or rejects.
  • Promise.any([...]) — settles with the first promise to fulfil; only rejects if they all reject.
// Fetch three resources at once, fail if any fails
const [user, posts, prefs] = await Promise.all([
  fetch('/api/user').then((r) => r.json()),
  fetch('/api/posts').then((r) => r.json()),
  fetch('/api/prefs').then((r) => r.json()),
]);

// Same, but tolerate individual failures
const results = await Promise.allSettled([taskA(), taskB(), taskC()]);
results.forEach((r) => {
  if (r.status === 'fulfilled') console.log('ok', r.value);
  else console.warn('failed', r.reason);
});

Common gotchas

  • Forgetting to return. Inside a .then(), if you call a promise but don't return it, the chain won't wait for it — you lose ordering and error handling.
  • Unhandled rejections. A promise without a .catch() (or a try/catch around await) produces an unhandledrejection. Always handle errors at the end of the chain.
  • Mixing callbacks and promises. Don't call resolve twice or both resolve and reject — only the first call counts; the rest are silently ignored.
  • The executor runs immediately. The function you pass to new Promise() runs synchronously, right away — only the .then() callbacks are deferred.

Quick reference

GoalMethod
Handle success.then(onFulfilled)
Handle failure.catch(onRejected)
Cleanup either way.finally(fn)
All must succeedPromise.all
Collect every outcomePromise.allSettled
First to settlePromise.race
First to succeedPromise.any

Promises are the plumbing under async/await, which is just nicer syntax over the same objects. Once chaining and the four combinators feel natural, asynchronous JavaScript stops being intimidating.