Promise.all waits for every promise to fulfill and fails fast on the first rejection. Promise.allSettled waits for every promise to finish and always fulfills with an array of {status, value|reason} results. Promise.race settles with whichever promise settles first, success or failure. Use all when one failure should cancel the whole batch, allSettled when you need every outcome, and race for timeouts or "first response wins".
MDN's pages for these methods are the official reference. This guide shows the behaviour with real timings I measured on Node.js 24.22.1 (and confirmed the methods exist on Node 20.20.2 and 18.20.8). If async/await itself is still fuzzy, start with my async/await tutorial, then come back here for the combinators.
Helper used in every timing test:
function delay(ms, value, shouldReject = false) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldReject) reject(new Error(String(value)));
else resolve(value);
}, ms);
});
}
In this guide:
- At a glance: all vs allSettled vs race (and any)
- Promise.all: success path and order
- Promise.all short-circuits on the first rejection
- Promise.allSettled: every outcome, always fulfills
- Practical allSettled pattern: keep the good results
- Promise.race: first settle wins (fulfill or reject)
- Timeout with Promise.race
- Promise.any: first fulfill, ignore rejects (brief)
- Empty iterables: the sharp edges
- When to use each (decision guide)
- Node versions I checked
- Frequently asked questions
At a glance: all vs allSettled vs race (and any)
| Method | Waits for | On first reject | Result shape | Empty input |
|---|---|---|---|---|
Promise.all |
All fulfill (or first reject) | Rejects immediately with that reason | Array of values, same order | Fulfills with []
|
Promise.allSettled |
All settle | Keeps waiting | Array of `{status, value | reason}` |
Promise.race |
First to settle | Rejects if that first settle is a reject | That one value or reason | Pending forever |
Promise.any |
First fulfill | Ignores rejects until all fail | First value, or AggregateError
|
AggregateError with empty errors |
I include Promise.any briefly because teams often reach for race when they meant "first success". They are not the same. A quick rule of thumb: if a single failure should fail the request, reach for all. If you are aggregating independent sources and can show a partial UI, reach for allSettled. If you are racing a timer, reach for race. If you are trying mirrors or fallbacks and only need one good answer, reach for any.
All four methods accept thenables and plain values as well as Promises. A plain string is treated as an already-fulfilled value. That is handy in tests, and it is how mixed arrays like [fetch(url), "cached"] still make sense as a list of outcomes.
Promise.all: success path and order
Promise.all takes an iterable of promises (or thenables / plain values) and fulfills with an array of results in the same order as the input, not the order they finished:
const stamp = () => Date.now();
const t0 = stamp();
const result = await Promise.all([
delay(40, 'a'),
delay(20, 'b'),
delay(60, 'c'),
]);
console.log('result:', result);
console.log('elapsed_ms:', stamp() - t0);
result: [ 'a', 'b', 'c' ]
elapsed_ms: 61
Elapsed time tracked the slowest promise (~60 ms), and the array stayed ['a','b','c'] even though 'b' finished first. Another check with a slow first slot and a fast second slot still returned ['first-slot', 'second-slot']. MDN's Promise.all page describes this ordering rule.
Promise.all short-circuits on the first rejection
This is the behaviour that surprises people. The returned promise rejects as soon as any input rejects. Other work is not cancelled; all just stops waiting for it:
const t0 = Date.now();
const flags = { a: false, b: false, c: false };
try {
await Promise.all([
delay(80, 'a').then((v) => { flags.a = true; return v; }),
delay(20, 'boom', true).then((v) => { flags.b = true; return v; }),
delay(100, 'c').then((v) => { flags.c = true; return v; }),
]);
} catch (e) {
console.log('caught:', e.message);
console.log('elapsed_ms:', Date.now() - t0);
await delay(120, 'wait');
console.log('flags_after_wait:', flags);
}
caught: boom
elapsed_ms: 21
flags_after_wait: { a: true, b: false, c: true }
Rejection arrived at ~20 ms with the fast failure. After a short wait, a and c had still run to completion. all does not abort in-flight work. If you need cancellation, wire AbortController into the underlying tasks yourself.
Use all when the batch is useless without every result: loading config that must all succeed, or a transaction-style fan-out where one failure should fail the request.
One more detail from the run: the rejected branch never set flags.b = true, because the .then on that promise never ran; the rejection skipped it. The other two .then callbacks still ran later. So "fail fast" describes the combined promise, not the sibling tasks. Logging, metrics and lock releases inside those siblings still need their own try/finally or AbortSignal handling.
Wrapping Promise.all in try/catch with async/await is the usual style in application code. The catch receives the first rejection reason, which is often an Error but can be any thrown value.
Promise.allSettled: every outcome, always fulfills
allSettled waits for every promise to settle and fulfills with a status object for each. Rejections become data, not an exception from allSettled itself:
const t0 = Date.now();
const result = await Promise.allSettled([
delay(40, 'ok'),
delay(20, 'nope', true),
delay(60, 42),
]);
console.log('elapsed_ms:', Date.now() - t0);
for (const r of result) {
if (r.status === 'fulfilled') console.log('fulfilled:', r.value);
else console.log('rejected:', r.reason.message);
}
elapsed_ms: 59
fulfilled: ok
rejected: nope
fulfilled: 42
Elapsed time matched the slowest input (~60 ms), not the first rejection. Fulfilled entries have value; rejected entries have reason. See MDN's Promise.allSettled reference for the exact shape.
Practical allSettled pattern: keep the good results
A common API fan-out wants every response it can get:
const urls = ['ok', 'bad', 'ok2'];
const settled = await Promise.allSettled(
urls.map(async (u) => {
if (u === 'bad') throw new Error('404 ' + u);
return 'fetched ' + u;
})
);
const ok = settled.filter((r) => r.status === 'fulfilled').map((r) => r.value);
const bad = settled.filter((r) => r.status === 'rejected').map((r) => r.reason.message);
console.log('ok:', ok);
console.log('bad:', bad);
ok: [ 'fetched ok', 'fetched ok2' ]
bad: [ '404 bad' ]
Use allSettled for dashboards, health checks, "fetch these ten feeds", or anything where partial success is still useful. If a rejected reason isn't an Error, log it as-is; don't assume .message always exists.
A small helper I use in scripts:
function partitionSettled(settled) {
return {
values: settled.filter((r) => r.status === 'fulfilled').map((r) => r.value),
reasons: settled.filter((r) => r.status === 'rejected').map((r) => r.reason),
};
}
Pass the array from allSettled straight in. You get two lists and no thrown exception. That is usually easier to render than unpacking status objects inline in JSX or a template.
Promise.race: first settle wins (fulfill or reject)
race settles with the first input that settles, whether that settle is a fulfill or a reject:
const t0 = Date.now();
const result = await Promise.race([
delay(80, 'slow'),
delay(20, 'fast'),
delay(40, 'mid'),
]);
console.log('result:', result);
console.log('elapsed_ms:', Date.now() - t0);
result: fast
elapsed_ms: 20
When the fastest promise rejects, the race rejects:
const t0 = Date.now();
try {
await Promise.race([
delay(80, 'slow-ok'),
delay(15, 'fast-fail', true),
delay(40, 'mid-ok'),
]);
} catch (e) {
console.log('caught:', e.message);
console.log('elapsed_ms:', Date.now() - t0);
}
caught: fast-fail
elapsed_ms: 14
MDN's Promise.race page is the short official description. The classic use is a timeout wrapper: race your work against a rejecting timer.
Timeout with Promise.race
Here's a tiny timeout helper built on race:
function withTimeout(promise, ms) {
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
// usage sketch:
// await withTimeout(fetch(url), 2000);
Clearing the timer in finally avoids leaking handles when the real work wins. Remember that losing the race does not cancel fetch unless you also pass an AbortSignal. For "wait until the first success, ignore earlier failures", use Promise.any instead of race.
I also use race for "prefer cache, but don't wait forever":
const data = await Promise.race([
readCache(key), // may fulfill fast
delay(50, null).then(() => null) // give up on cache
]);
if (data == null) {
// fall through to network
}
If the cache is slow or missing, the timer wins with null and you continue. If the cache is warm, you skip the network. Pairing that with allSettled is overkill; race is the right shape for "first useful signal".
Promise.any: first fulfill, ignore rejects (brief)
Promise.any fulfills with the first fulfilled value and only rejects when every input rejects, via AggregateError:
const t0 = Date.now();
const result = await Promise.any([
delay(20, 'first-fail', true),
delay(50, 'winner'),
delay(80, 'late'),
]);
console.log('result:', result);
console.log('elapsed_ms:', Date.now() - t0);
result: winner
elapsed_ms: 50
try {
await Promise.any([
delay(10, 'a', true),
delay(20, 'b', true),
]);
} catch (e) {
console.log('name:', e.name);
console.log('errors:', e.errors.map((x) => x.message));
}
name: AggregateError
errors: [ 'a', 'b' ]
That is the opposite of race when early failures are noise. MDN documents it under Promise.any. It exists on Node 18+, 20+ and 24 in my checks.
A practical "try these mirrors" sketch:
const mirrors = [
fetch('https://mirror-a.example/file'),
fetch('https://mirror-b.example/file'),
fetch('https://mirror-c.example/file'),
];
const res = await Promise.any(mirrors);
// first 2xx/ok response body wins; early network errors are ignored
I did not hit real mirrors in this batch; the timing tests used delay instead. The control flow matches what you want for CDN fallbacks: ignore early failures, take the first success, and only fail when every mirror fails.
Empty iterables: the sharp edges
Empty input is where these methods disagree the most:
console.log('all []:', await Promise.all([]));
console.log('allSettled []:', await Promise.allSettled([]));
let raceSettled = false;
const raceP = Promise.race([]);
raceP.then(() => { raceSettled = true; }, () => { raceSettled = true; });
await delay(50, 'tick');
console.log('race [] settled within 50ms?', raceSettled);
try {
await Promise.any([]);
} catch (e) {
console.log('any [] name:', e.name, 'errors length:', e.errors.length);
}
all []: []
allSettled []: []
race [] settled within 50ms? false
any [] name: AggregateError errors length: 0
all and allSettled treat "nothing to wait for" as done. race([]) never settles. any([]) rejects immediately with an empty AggregateError. Guard empty lists before you race them. Inside an async mapper, reading a property off undefined is a different crash; see Cannot read properties of undefined.
When to use each (decision guide)
- Promise.all — every result is required; one failure should fail the whole operation.
- Promise.allSettled — you want a report of successes and failures; partial data is useful.
- Promise.race — timeouts, first response wins, or "whichever finishes first, good or bad".
- Promise.any — first success wins; early failures should be ignored.
If you catch yourself writing nested try/catch around Promise.all just to keep going after a failure, stop and switch to allSettled. If you catch yourself writing race and then ignoring rejections manually, stop and switch to any. The methods already encode those policies; re-implementing them by hand is how subtle timing bugs creep in.
If you call something that isn't a function while mapping promises, you'll see a different error. My post on TypeError: X is not a function covers that.
Node versions I checked
All timings above are from Node.js 24.22.1 running test/js/demo.mjs. Quick existence checks:
// Node 20.20.2 and 18.20.8 both printed:
{
all: 'function',
allSettled: 'function',
race: 'function',
any: 'function'
}
allSettled and any are widely available in modern Node and current browsers. If you still support very old engines, check your baseline before relying on any. The combinators themselves are part of the language; Node's globals documentation lists Promise among the standard globals.
For day-to-day debugging I keep the four one-liners nearby:
-
await Promise.all(tasks)for all-or-nothing -
await Promise.allSettled(tasks)for a full report -
await Promise.race([work, timeout])for deadlines -
await Promise.any(mirrors)for first success
Everything else is choosing the right one for the failure mode you actually want, then testing it with deliberate delays the way I did above.
Frequently asked questions
What is the difference between Promise.all and Promise.allSettled?
Promise.all fulfills with all values or rejects on the first rejection. Promise.allSettled always fulfills with an array of status objects describing each outcome, and never rejects just because an input rejected.
Does Promise.all cancel the other promises when one fails?
No. It rejects your combined promise early, but the other async work keeps running unless you cancel it yourself (for example with AbortController).
When should I use Promise.race?
Use race for timeouts or when the first settled result should win, including failures. If you only care about the first success, use Promise.any instead.
What does Promise.any do that race does not?
any ignores rejections until every input has rejected, then throws AggregateError. race settles on the first rejection or fulfillment, whichever comes first.
What happens with Promise.all([])?
It fulfills immediately with an empty array. allSettled([]) does the same. race([]) stays pending. any([]) rejects with AggregateError.
Is the result order of Promise.all the finish order?
No. Results stay in input order. A faster second promise still lands in index 1.
How do I collect only successful results from allSettled?
Filter for status === 'fulfilled' and map to .value. Map rejected entries via .reason for logging.
Can I use these with async/await?
Yes. await Promise.all(...), await Promise.allSettled(...), and so on. The await just waits on the combined promise the method returns.
Last checked: 6 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.
Originally published on mfaysal.com.
I'm learning cybersecurity on TryHackMe — follow my progress at tryhackme.com/p/mfaysal.dev.
Top comments (0)