Case 7/13

Prefer reading? The full explanation

How Promise.all Runs

fail-fast, but nothing gets cancelled · Browser

Before this

How Rejections Run — how a rejection travels through handlers.

After this you can explain

What fail-fast actually settles, and why the "losing" promises keep running unless you cancel them yourself.

const fast = new Promise((_, reject) =>
  setTimeout(() => reject(new Error('fast failed')), 10));

const slow = new Promise((resolve) =>
  setTimeout(() => { console.log('2: slow finished anyway'); resolve('ok'); }, 50));

Promise.all([fast, slow])
  .then((v) => console.log('all:', v))
  .catch((e) => console.log('1: rejected:', e.message));

console.log('0: sync');

Output: 0: sync → 1: rejected: fast failed → 2: slow finished anyway

Why does the slow promise still finish?

The output is 0 → 1 → 2. The combined promise rejects at ~10ms — that is fail-fast, and it is real. But 2: slow finished anyway still prints at ~50ms.

Promise.all is a subscriber, not a supervisor. It attaches reactions to its inputs and settles its own promise based on what they report. It holds no reference to the timer, the request, or whatever work backs each input, so it has nothing it could cancel. Rejection changes who is listening, never what is running.

What fail-fast buys you, precisely

Fail-fast means your .catch learns about the failure at the earliest possible moment — you can render the error state at 10ms instead of 50ms. That is the entire benefit: earlier notification.

It is worth internalising the asymmetry: fulfilment requires every input, rejection requires just one. all resolves with an array of every value, or rejects with the single first error. Later rejections among the inputs are ignored by the combiner — and because all attached handlers to every input, they do not count as unhandled either.

The combinators differ in when they settle, not what they cancel

None of them cancels anything. allSettled waits for every input and never rejects — the first experiment shows the failure being recorded while the combinator keeps waiting. race settles with the first input to settle, win or lose. any fulfils with the first fulfilment and only rejects if every input rejects.

Pick by the question you are asking: "I need all of it" (all), "I need a report on all of it" (allSettled), "first response wins" (race / any). In every case, the losing work keeps running.

When ignoring is not enough

Discarding results is fine when the losing work is cheap and side-effect-free. It is not fine when the work holds a connection open, writes somewhere, or costs money per request.

Real cancellation has to reach the work itself, and the platform's protocol for that is AbortSignal: create an AbortController, pass its signal into each fetch (or your own async work), and call abort() in the catch. Aborting stops the underlying work and rejects the in-flight promises with an AbortError — so their fulfilment handlers never run, while any catch and finally attached to them still do. Handle the abort rejection deliberately rather than expecting silence.

Reading about it is one thing. Watching it run is another.

↑ Run it step by step

In the wild

Where this rule bites real code

The dashboard that pays for requests it throws away

Symptom
A page fires six API calls through Promise.all. One flaky endpoint rejects; the UI shows an error — yet the network tab shows all six requests completing, and your API bill counts all six.
Why
all() rejecting only changed who listens. The five in-flight requests keep transferring and completing; their responses arrive to nobody.
Fix
Share one AbortController across the batch and call .abort() in the catch — fetch rejects immediately and the transfers actually stop.

The partial failure that hid the good data

Symptom
Nine of ten widgets loaded fine, but one failed — and the whole dashboard rendered the error state.
Why
all() is all-or-nothing by design: one rejection and the values of the other nine are unreachable through it.
Fix
Use allSettled when partial results are useful. Render the fulfilled ones, show per-widget errors for the rest.

Environment

Browser

CI verified

Node.js v22.22.3

Last reviewed

29 July 2026

The animation on this page is a teaching model, not a diagram of browser internals. Real engines use several task queues, optimise aggressively, and do plenty of work these panels do not show. What is guaranteed is the observable ordering: the stated output is produced by actually executing the code in CI, on the runtime named above. Browser-specific behaviour follows the HTML Standard and is not executed in a real browser by CI. How this is verified →