Promise.all vs allSettled vs race vs any in JavaScript
Promise.all fails fast, allSettled reports every outcome, race takes the first to settle and any the first to fulfil. Plus concurrency limits and cancelling.
- Course: JavaScript study plan
- Module: Asynchronous JavaScript
- Kind: Lesson
- Reading time: 14 min
- Runtime: Node 16
What is the difference between Promise.all, allSettled, race and any?
Promise.all fulfils with every value in input order and rejects on the first rejection. Promise.allSettled always fulfils, once everything settles, with a { status, value } or { status, reason } object for each input. Promise.race settles like the first input to settle, fulfilled or rejected. Promise.any fulfils with the first fulfilment and rejects with an AggregateError only if every input rejects.
Lesson
Once every asynchronous operation is a promise, coordinating many of them becomes algebra: wait for all, wait for any, wait for the first, wait for all regardless of outcome. The four Promise combinators cover the common shapes; three patterns you write yourself cover the rest — a concurrency limit so a thousand requests do not all start at once, a timeout so nothing waits forever, and cancellation through AbortController so abandoned work stops. This lesson covers each, plus async iteration (for await) for sequences that arrive over time.
The four combinators
const [user, team] = await Promise.all([fetchUser(id), fetchTeam(tid)]);
| Combinator | Fulfils when | Rejects when | Result |
|---|---|---|---|
Promise.all(list) | every promise fulfils | the first rejection (others keep running, results dropped) | array of values, in input order |
Promise.allSettled(list) | always, once all settle | never | array of { status: "fulfilled", value } / { status: "rejected", reason } |
Promise.race(list) | the first to settle fulfils | the first to settle rejects | that value / reason |
Promise.any(list) | the first to fulfil | all reject → AggregateError with .errors | that value |
All four accept any iterable of promises or plain values (values are wrapped). all is the workhorse: fan out, wait, get results in order. allSettled is for batches where one failure must not hide the others. race is the building block for timeouts. any is "first success wins" — redundant sources, fastest mirror. An empty all/allSettled fulfils with [] immediately; an empty race stays pending forever; an empty any rejects.
Two details: rejection in all does not cancel the others — they finish and their results vanish; and each combinator attaches handlers to every input, so an input that rejects after all already rejected is still "handled" (no unhandled-rejection crash).
Bounded concurrency
Promise.all(urls.map(fetch)) starts every request at once — fine for five, a self-inflicted denial of service for five thousand. A pool runs at most n at a time:
async function mapLimit(items, limit, worker) {
const results = new Array(items.length);
let next = 0;
async function runner() {
while (next < items.length) {
const i = next++; // claim an index — safe: no await between read and increment
results[i] = await worker(items[i], i);
}
}
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, runner));
return results;
}
limit runners each pull the next index when they finish — a work queue with no explicit queue object. Results land by index, so order is preserved. Real code adds error policy (fail fast versus collect via the allSettled shape) and this is exactly what libraries like p-limit/p-map do.
Timeouts
function withTimeout(promise, ms, message = `timed out after ${ms} ms`) {
let timer;
const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), ms); });
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
race against a timer. The finally clears the timer so a fast result does not leave a pending timeout keeping the process alive (and firing a rejection nobody handles — race handled it, but the timer itself still runs). Node 16 also has require("timers/promises").setTimeout(ms, value, { signal }) — a promise-based sleep that accepts an abort signal.
Cancellation: AbortController
Promises cannot be cancelled — a settled-once value has no "stop" — so cancellation is a signal passed into the operation, which checks it:
const controller = new AbortController();
setTimeout(() => controller.abort(), 500); // give up after half a second
try {
const res = await fetch(url, { signal: controller.signal }); // fetch rejects with an AbortError on abort
} catch (err) {
if (err.name === "AbortError") console.log("cancelled"); else throw err;
}
// honouring a signal in your own code
async function poll(fn, { signal }) {
while (!signal.aborted) {
await fn();
await timersPromises.setTimeout(1000, undefined, { signal }); // sleeps that end early on abort
}
signal.throwIfAborted?.(); // Node 17.3+; on 16 throw manually
}
signal.aborted, signal.addEventListener("abort", …), and signal.reason (Node 17.2+) are the surface. fetch, timers/promises, fs.promises.readFile, streams and events.once all accept a signal. Abort rejects with an error named AbortError — treat it as "stop", not as failure. AbortSignal.timeout(ms) (Node 16.14+) builds the timeout case directly — on older runtimes combine a controller with a timer.
Retry with delay
async function retry(fn, { attempts = 3, delayMs = 100, isTransient = () => true } = {}) {
for (let i = 1; ; i++) {
try { return await fn(i); }
catch (err) {
if (i >= attempts || !isTransient(err)) throw err;
await sleep(delayMs * 2 ** (i - 1)); // exponential backoff: 100, 200, 400 …
}
}
}
Module 7's rules — transient only, idempotent only, bounded — plus a delay that grows (and, in production, a little random jitter so a thousand clients do not retry in lockstep).
Async iteration
for await (const chunk of stream) { … } // any object with [Symbol.asyncIterator]
async function* pages(url) { // an async generator: yield inside, await allowed
while (url) { const page = await fetchJson(url); yield* page.items; url = page.next; }
}
for await (const item of pages(first)) { if (enough(item)) break; } // break → return() → cleanup
for await awaits each next(); it also accepts a sync iterable of promises, awaiting each in order. Async generators are the natural producer for paginated APIs, line-by-line file reading (readline), and event streams (events.on(emitter, "data") returns one). Items are processed sequentially — one at a time, in order — which is often the point.
Events to promises
const [value] = await events.once(emitter, "ready") (Node) resolves on the first emission and rejects on "error". For a browser EventTarget, wrap addEventListener(…, { once: true }) in new Promise. Streams: stream/promises pipeline and finished.
Common mistakes
Promise.allfor a batch where partial results matter (useallSettled); expectingallto cancel the rest on failure.- Unbounded
map(fetch)over large lists. - A timeout
racethat does not clear its timer; a timeout that does not abort the underlying operation (it keeps running). - Treating
AbortErroras a failure to log loudly. - Retrying without backoff, jitter or a bound.
for awaitwhen concurrency was wanted (it is sequential by design).
Interview angle
- "
Promise.allversusallSettled?"allrejects on the first failure and drops the rest;allSettledalways fulfils with every outcome. - "How do you limit concurrency?" A pool of
nrunners pulling from a shared index/queue; results stored by index. - "How do you add a timeout to a promise?"
raceagainst a timer, clear the timer infinally, and abort the operation with a signal if it supports one. - "How do you cancel a promise?" You cannot; pass an
AbortSignalthe operation honours and reject withAbortError. - "
raceversusany?" First to settle (including rejection) versus first to fulfil.
Key takeaways
all(values in order, fail fast),allSettled(every outcome),race(first settle),any(first fulfil,AggregateErrorif none).- Bound concurrency with a runner pool; it preserves order by index.
- Timeout =
race+ timer +clearTimeoutinfinally; cancellation =AbortController→AbortError, honoured byfetch, timers and fs. - Retry transient, idempotent operations with bounded exponential backoff and jitter.
for awaitand async generators process sequences that arrive over time, one item at a time.
Common questions
Does Promise.all cancel the other promises when one rejects?
No. Promise.all rejects as soon as one input rejects, but the other operations keep running to completion and their results are dropped. Promises cannot be cancelled; stopping the work needs an AbortSignal that the operations honour.
How do you limit concurrency with promises in JavaScript?
Run a pool: start limit async runners that each claim the next index from a shared counter, await the worker for that item and store the result at that index, then await Promise.all over the runners. At most limit tasks run at once and results keep their order — what p-limit and p-map do.
How do you add a timeout to a promise?
Race it against a timer that rejects — Promise.race([promise, timeout]) — and clear the timer in finally so a fast result does not leave it pending. The race only stops the waiting; pass an AbortSignal as well if the operation itself should stop.
How do you cancel a promise in JavaScript?
You cannot cancel the promise itself. Pass an AbortController's signal into the operation — fetch, timers/promises, fs.promises.readFile and streams accept one — and call controller.abort(); the operation rejects with an error named AbortError, which means stop, not failure.
What does for await…of do in JavaScript?
for await (const x of source) loops over an async iterable, awaiting each next(), so items are processed one at a time and in order. It suits paginated APIs, streams and line-by-line file reading, usually fed by an async generator, and it also accepts a sync iterable of promises.
Exercises
The four combinators on one set of tasks
Each line is name ms ok|fail; start(spec) (in the starter) returns a promise that fulfils with the name or rejects with Error("<name> failed") after ms. Run fresh promises through each combinator and print: all: <names joined by ,> or all: rejected <message>; allSettled: <name:status ...>; race: <name> fulfilled or race: <name> rejected; any: <name> or any: AggregateError with <n> errors.
Example: a 30 ok, b 10 ok, c 20 fail →
all: rejected c failed
allSettled: a:fulfilled b:fulfilled c:rejected
race: b fulfilled
any: bA concurrency pool and a timeout
Write mapLimit(items, limit, worker) — at most limit workers run at once, results stored by index — and withTimeout(promise, ms) — a race against a timer that rejects with Error("timed out after <ms> ms"), clearing the timer in finally. Line 1 is the limit; following lines are tasks name ms, plus one timeout <taskMs> <limitMs> line. Run the tasks through the pool (each worker sleeps ms, tracks the running count, records completion order and returns the upper-cased name) and print results=<in input order> completed=<completion order> maxConcurrent=<n>; then run withTimeout(sleep(taskMs, "finished"), limitMs) and print timeout: finished or timeout: <message>.
Example: 2, a 40, b 10, c 15, d 5, timeout 50 10 →
results=A,B,C,D completed=b,c,d,a maxConcurrent=2
timeout: timed out after 10 ms
b finishes at 10 and frees a slot for c (done at 25), then d (30); a runs until 40.
In this module: Asynchronous JavaScript
- Callbacks and the event loop — one thread, two queues
- Promises — a value that is not here yet
- async/await — promises with the syntax of ordinary code
- Combinators, concurrency limits, timeouts and cancellation (this lesson)
- Timers and scheduling — setTimeout, setInterval, nextTick, and debounce/throttle
- Checkpoint — Asynchronous JavaScript
← async/await — promises with the syntax of ordinary code · Timers and scheduling — setTimeout, setInterval, nextTick, and debounce/throttle →