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