codejump
Academy

Promise.all, allSettled, race and any

The four ways to combine promises, what each one does with a rejection, and how to pick the right one without guessing.

Updated

Four combinators, two questions

All four take an iterable of promises and return one promise. They differ on exactly two questions: when does it settle, and what does a rejection do.

Settles whenOn rejection
Promise.allall fulfilrejects immediately with the first error
Promise.allSettledall settlenever rejects — reports it as a result
Promise.racethe first one settlesrejects if the first to settle rejected
Promise.anythe first one fulfilsrejects only if all reject, with an AggregateError

Read the table once and most "which one do I use" questions answer themselves.

all — everything, or nothing

const [user, orders] = await Promise.all([
  fetchUser(id),
  fetchOrders(id),
]);

Both requests start immediately and run concurrently. The result array is in input order, not completion order — this is the property people rely on without noticing.

The catch is the fail-fast behaviour. If fetchOrders rejects, you get that error and fetchUser's result is discarded — but its request is not cancelled. A rejected Promise.all does not stop the other work; it just stops you from seeing it.

allSettled — when a partial answer is still an answer

const results = await Promise.allSettled(urls.map(fetchOne));

const ok = results.filter((r) => r.status === 'fulfilled').map((r) => r.value);
const failed = results.filter((r) => r.status === 'rejected');

Every entry is { status: 'fulfilled', value } or { status: 'rejected', reason }. The combined promise never rejects, so a try/catch around it catches nothing — the errors are data now, and you have to look at them.

Use it whenever one failure should not void the batch: sending notifications, warming caches, importing rows.

race — the first to settle, whatever it is

const result = await Promise.race([
  fetchData(),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error('timeout')), 5000)
  ),
]);

This is the timeout pattern, and it is what race is really for. Note the asymmetry: if fetchData rejects in 10 ms, race rejects in 10 ms — it does not wait for a happier outcome. That is the difference from any, and it is the reason a "first successful response" implementation built on race is a bug waiting for its first flaky endpoint.

any — the first success, ignoring failures

const fastest = await Promise.any([
  fetch(mirrorA),
  fetch(mirrorB),
  fetch(mirrorC),
]);

Rejections are tolerated until there are no promises left. If all reject you get an AggregateError, whose errors array holds every reason in input order.

Three details that cause real bugs

An empty array is not an edge case you can ignore. Promise.all([]) fulfils immediately with []. Promise.any([]) rejects immediately with an AggregateError. Promise.race([]) never settles at all — it hangs, silently, forever.

The promises start before the combinator does. .map(fetchOne) fires every request as the array is built. Promise.all only decides how to *wait*. If you need to limit concurrency, the combinator is the wrong tool — you need a queue.

Non-promise values are allowed. Anything that is not a thenable is wrapped with Promise.resolve, so Promise.all([1, fetchX()]) works and puts 1 in the first slot.

Rejection handlers must be attached *when the promise is created*, not later. Building an array of promises, awaiting something else, then passing them to allSettled can produce an unhandled rejection warning in between — the promise rejected while nobody was listening.

Now practice it

Reading this page is the cheap half. These are the exercises that make you use it.