JavaScript gives you four ways to combine multiple promises: Promise.all, Promise.allSettled, Promise.race, and Promise.any. They all start their promises concurrently, but they differ completely in when they settle and what they give you back. Pick the wrong one and you get silent bugs — a dashboard that shows nothing because one widget failed, or a timeout that never fires.
Most tutorials hand you a four-row comparison table and call it done. The problem is that tables don’t show timing — and timing is the entire point of these methods. This guide includes a live visualiser: set four promises with different durations and outcomes, hit run, and watch all four methods settle in real time so you can see the difference instead of memorising it.
These methods build directly on the event loop and the mistakes covered in the async/await debugging guide — if await inside a loop has ever surprised you, these combinators are the fix.
Live Demo
Set the duration and outcome of four promises, then run all four methods at once. Watch the timeline show exactly when each method settles and what it returns. Controls lock during a run to keep the animation in sync.
Promise.all vs allSettled vs race vs any — The One-Line Summary
| Method | Settles when | Returns | Rejects when |
|---|---|---|---|
Promise.all | all fulfil | array of values (input order) | any rejects (immediately) |
Promise.allSettled | all settle | array of {status, value/reason} | never rejects |
Promise.race | first settles | that value/reason | first settle is a rejection |
Promise.any | first fulfils | that value | all reject (AggregateError) |
Two axes explain all four:
Wait for ALL Wait for FIRST
┌────────────────────┬────────────────────┐
Fail-fast │ Promise.all │ Promise.race │
│ (reject on any) │ (settle on first) │
├────────────────────┼────────────────────┤
Tolerant │ Promise.allSettled│ Promise.any │
│ (never rejects) │ (reject if all do)│
└────────────────────┴────────────────────┘
Promise.all vs allSettled comes down to one question: are partial results useful? If yes, allSettled. If no, all.
Browser/runtime support anchors:
Promise.all,Promise.race— ES2015 (everywhere)Promise.allSettled— ES2020 (Node 12.9+, all evergreen browsers)Promise.any+AggregateError— ES2021 (Node 15+, all evergreen browsers)Promise.withResolvers()— ES2024 (Chrome 119+, Firefox 121+, Safari 17.4+, Node 22+)Promise.try()— ES2025 (Chrome 128+, Firefox 134+, Safari 18.2+, Node 23+)
Promise.all Error Handling — All or Nothing
Resolves with an array of all results once every promise fulfils. Rejects immediately the moment any one rejects — it does not wait for the others.
const [user, posts, settings] = await Promise.all([
fetchUser(id),
fetchPosts(id),
fetchSettings(id),
]);
// All three available, or the whole thing throws
Key behaviour — fail-fast: If fetchPosts rejects at 100ms, Promise.all rejects at 100ms even if the other two are still pending. The other promises keep running (promises cannot be cancelled), but their results are discarded.
Use it when: every result is required and partial data is useless — rendering a page that needs user + posts + settings all present.
Promise.all error handling is all-or-nothing — one rejection discards every fulfilled value. The pattern below combined with .catch recovers from that:
// Convert each promise to "never reject" before passing to Promise.all
const [user, posts, settings] = await Promise.all([
fetchUser(id).catch(() => DEFAULT_USER),
fetchPosts(id).catch(() => []),
fetchSettings(id).catch(() => DEFAULT_SETTINGS),
]);
But if you’re going to write .catch on every promise, you really want Promise.allSettled instead — see below.
The Order Trap Nobody Mentions
The results array is in input order, not settle order. Even if fetchSettings finishes first, it is still at index 2:
const results = await Promise.all([slow(), fast(), medium()]);
// results[0] = slow's value (even though it finished last)
// results[1] = fast's value
// results[2] = medium's value
// Order matches the INPUT array, not completion order
Promise.all Parallel Requests — Tuple Inference in TypeScript
Promise.all parallel requests run concurrently and finish in roughly the time of the slowest one. In TypeScript, the tuple type is inferred automatically when you pass an array literal:
const [user, posts, settings] = await Promise.all<
[User, Post[], Settings]
>([fetchUser(id), fetchPosts(id), fetchSettings(id)]);
// TypeScript narrows each destructured variable to its precise type
Modern TypeScript (4.7+) infers the tuple even without the explicit type argument if you pass a literal array. Use the explicit form when the array is built up dynamically.
Promise.allSettled — Never Rejects, Reports Everything
Waits for every promise to settle (fulfil or reject) and returns an array describing each outcome. It never rejects — you inspect each result’s status.
const results = await Promise.allSettled([
fetchWidget('weather'),
fetchWidget('stocks'),
fetchWidget('news'),
]);
const loaded = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
renderDashboard(loaded); // show what worked
logErrors(failed); // retry/log what didn't
Each result is either { status: 'fulfilled', value: ... } or { status: 'rejected', reason: ... }.
Use it when: partial success is acceptable and you want to know what failed — dashboards, batch operations, loading multiple independent widgets. This is the correct default for most “load several things” scenarios.
Promise.allSettled TypeScript — The Narrowing Fix
This is the most-searched TypeScript+combinator issue. Plain .filter(r => r.status === 'fulfilled') doesn’t narrow r to the PromiseFulfilledResult<T> shape, so accessing r.value errors with “Property ‘value’ does not exist on type ‘PromiseRejectedResult’.”
The fix is a type predicate:
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
const values = results
.filter((r): r is PromiseFulfilledResult<string> => r.status === 'fulfilled')
.map(r => r.value); // ✓ TypeScript knows r.value is string
const reasons = results
.filter((r): r is PromiseRejectedResult => r.status === 'rejected')
.map(r => r.reason); // ✓ TypeScript knows r.reason is unknown
TypeScript issue #42012 tracks the request for automatic narrowing; until then, the type predicate is the canonical workaround.
Recovering From Partial Failure: allSettled → Fallback Pattern
The dashboard example shows what worked and what didn’t, but often you want a single results array with sensible fallbacks for the failed ones:
const widgets = await Promise.allSettled(
['weather', 'stocks', 'news'].map(name => fetchWidget(name))
);
// Map every result to a usable value — real data on success, fallback on failure
const usable = widgets.map((r, i) => {
if (r.status === 'fulfilled') return r.value;
console.warn(`Widget ${i} failed:`, r.reason);
return { type: 'fallback', message: 'Could not load' };
});
renderDashboard(usable); // every slot filled, no missing tiles
Promise.race Example — First to Settle Wins
Settles as soon as the first promise settles — whether it fulfils or rejects. The first one to finish determines the outcome.
const result = await Promise.race([
fetchData(),
timeout(5000), // rejects after 5s
]);
// Whichever settles first wins — data or timeout error
⚠ This is NOT the “race condition” from your CS textbook
In concurrency theory, a “race condition” is a bug where two threads access shared state in an undefined order.
Promise.raceis a deliberate selection mechanism — first settler wins, by design. Different concept, unfortunately overloaded name.
Promise.race + Memory: The Loser Pins Memory
If your losing promise holds a large response in memory, Promise.race settling does not free that memory — the loser continues to completion and only then is its result garbage collected. For large responses or long-running operations, always pair race with AbortController so the loser actually stops.
Modern AbortController Fetch Timeout — AbortSignal.timeout()
The classic JavaScript promise timeout pattern races your fetch against setTimeout. The modern code uses AbortSignal.timeout instead — it’s a one-liner that automatically aborts:
// Modern (Chrome 103+, Firefox 100+, Safari 16+) — drop the Promise.race
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
// Aborts automatically after 5s. The fetch is genuinely cancelled, not orphaned.
For non-fetch operations (database queries, file I/O), Promise.race is still the pattern — but you should pair it with explicit cancellation:
async function withTimeout(promise, ms) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(new Error(`Timed out after ${ms}ms`)), ms);
try {
const result = await promise;
return result;
} finally {
clearTimeout(timer);
}
}
AbortSignal.any() — Multiple Cancellation Sources
The missing piece for “abort if timeout fires OR user clicks cancel OR navigation happens.” AbortSignal.any([signal1, signal2, ...]) (Chrome 116+, Firefox 124+, Safari 17+) creates a single signal that fires when any of its inputs fire:
const userCancel = new AbortController();
const navAbort = new AbortController();
cancelBtn.addEventListener('click', () => userCancel.abort());
const combinedSignal = AbortSignal.any([
AbortSignal.timeout(10_000), // hard 10s timeout
userCancel.signal, // user clicked Cancel
navAbort.signal, // page is unloading
]);
try {
const res = await fetch('/api/slow', { signal: combinedSignal });
return await res.json();
} catch (err) {
if (err.name === 'AbortError') {
console.log('Cancelled by:', err.cause); // shows which signal aborted
}
throw err;
}
For modern timeout patterns, AbortSignal.any + AbortSignal.timeout is the recommended approach. Promise.race for timeouts is a legacy pattern — losers keep running, memory pins, and the cancellation story is incomplete.
Promise.any Example — First Success Wins
Settles with the first promise to fulfil, ignoring rejections. Only rejects if every promise rejects — and then with an AggregateError containing all the individual errors.
const fastest = await Promise.any([
fetch('https://mirror1.example.com/file'),
fetch('https://mirror2.example.com/file'),
fetch('https://mirror3.example.com/file'),
]);
// First mirror to respond wins — slow/failed mirrors ignored
Use it when: you have multiple sources for the same thing and want whichever responds first successfully — CDN mirrors, fallback APIs, redundant endpoints.
AggregateError JavaScript — The Shape When All Fail
When every promise rejects, Promise.any throws an AggregateError in JavaScript with an .errors array containing each individual rejection reason in input order:
try {
await Promise.any([Promise.reject('a'), Promise.reject('b')]);
} catch (err) {
console.log(err instanceof AggregateError); // true
console.log(err.errors); // ['a', 'b'] — all reasons in input order
console.log(err.message); // 'All promises were rejected'
}
This lets you inspect why each source failed when, for example, all your CDN mirrors are down. It is the main place AggregateError appears in everyday JavaScript.
Promise.any is the mirror image of Promise.all: all rejects on the first failure, any resolves on the first success.
Side-by-Side: Same Input, Four Outcomes
Given three promises — A fulfils at 100ms, B rejects at 50ms, C fulfils at 200ms:
| Method | Settles at | Result |
|---|---|---|
Promise.all | 50ms | rejects with B’s reason (fail-fast) |
Promise.allSettled | 200ms | array: [fulfilled A, rejected B, fulfilled C] |
Promise.race | 50ms | rejects with B’s reason (first to settle) |
Promise.any | 100ms | fulfils with A’s value (first success, ignores B) |
This single table is what the visualiser above animates — set these exact timings and watch all four settle at different moments.
Concurrent Promises in JavaScript — When Promise.all Is Too Much
Promise.all([thousandOfFetches]) is a known footgun. You’ll hit rate limits, exhaust sockets, run out of memory, or get throttled. Running too many concurrent promises in JavaScript demands a promise pool to limit concurrency — keeping N requests in flight at once and queuing the rest.
Hand-rolled promise pool (10 lines)
async function promisePool(items, mapper, concurrency = 5) {
const results = new Array(items.length);
let i = 0;
async function worker() {
while (i < items.length) {
const idx = i++;
results[idx] = await mapper(items[idx], idx);
}
}
await Promise.all(Array.from({ length: concurrency }, worker));
return results;
}
// Usage — fetch 1000 URLs, 5 at a time
const urls = [/* 1000 URLs */];
const responses = await promisePool(urls, url => fetch(url).then(r => r.json()), 5);
Each worker pulls the next item, awaits it, then pulls the next — so exactly concurrency operations are in flight at any moment.
The one-line library version
The canonical library is Sindre Sorhus’s p-limit:
import pLimit from 'p-limit';
const limit = pLimit(5);
const responses = await Promise.all(
urls.map(url => limit(() => fetch(url).then(r => r.json())))
);
// At most 5 fetches are in flight at any moment
When to use a pool vs Promise.all
| Use case | Use |
|---|---|
| 2-10 known requests | Promise.all |
| 10-100 requests, no rate limits | Promise.all (usually fine) |
| 100+ requests OR rate-limited API | Promise pool with concurrency cap |
| Streaming arbitrary number of items | for await...of with internal pool |
| Need first success across mirrors | Promise.any |
Real-World Patterns
Pattern 1 — Dashboard with partial success (allSettled)
async function loadDashboard() {
const widgets = ['weather', 'stocks', 'calendar', 'news'];
const results = await Promise.allSettled(
widgets.map(w => fetchWidget(w))
);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
renderWidget(widgets[i], result.value);
} else {
renderWidgetError(widgets[i], result.reason);
}
});
}
Pattern 2 — Timeout any operation (race) or AbortSignal
// Modern fetch
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
// Non-fetch operations (DB queries, etc.) — Promise.race + cleanup
function timeout(ms) {
return new Promise((_, reject) =>
setTimeout(() => reject(new Error('Operation timed out')), ms)
);
}
const result = await Promise.race([
runDatabaseQuery(),
timeout(5000),
]);
Pattern 3 — Fastest mirror with fallback (any)
async function fetchFromFastestMirror(path) {
const mirrors = ['cdn1', 'cdn2', 'cdn3'];
try {
return await Promise.any(
mirrors.map(m => fetch(`https://${m}.example.com${path}`))
);
} catch (err) {
// err is AggregateError — every mirror failed
throw new Error('All mirrors unreachable', { cause: err });
}
}
Pattern 4 — Required data, fail-fast (all)
async function renderProfilePage(userId) {
const [user, avatar, permissions] = await Promise.all([
fetchUser(userId),
fetchAvatar(userId),
fetchPermissions(userId),
]);
return buildProfile(user, avatar, permissions);
}
Beyond the Four Combinators: Promise.withResolvers() and Promise.try()
Two newer primitives that work well with the combinators above.
Promise.withResolvers() (ES2024)
Returns { promise, resolve, reject } — the deferred pattern that previously required the four-line let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); boilerplate:
function createDeferred() {
return Promise.withResolvers();
}
// Resolve from outside the promise constructor — useful for event-driven flows
const { promise, resolve, reject } = Promise.withResolvers();
button.addEventListener('click', () => resolve('user clicked'));
setTimeout(() => reject(new Error('timed out')), 5000);
const result = await promise;
Combine with Promise.race to build custom timeout primitives without the constructor dance.
Promise.try() (ES2025)
Wraps a function call so synchronous throws become rejections — replacing the new Promise((resolve) => resolve(fn())) idiom:
// Old way — if fn() throws synchronously, you get an unhandled throw
const p = new Promise((resolve) => resolve(maybeAsyncFn()));
// ES2025 — synchronous throws become Promise rejections
const p = Promise.try(maybeAsyncFn);
// Works whether maybeAsyncFn returns a value, a Promise, or throws
// Useful inside Promise.all when one of the inputs might be sync-throwing
const results = await Promise.all([
Promise.try(syncMaybeThrow),
Promise.try(asyncOperation),
Promise.try(() => 42),
]);
Both are progressive enhancement — feature-detect if you support older runtimes.
Edge Cases That Cause Bugs
Empty arrays behave differently:
await Promise.all([]); // resolves immediately with []
await Promise.allSettled([]); // resolves immediately with []
await Promise.race([]); // NEVER settles — hangs forever!
await Promise.any([]); // rejects immediately with AggregateError
Promise.race([]) hanging forever is a real production bug — always guard against empty arrays before racing.
Non-promise values are allowed — they are treated as already-fulfilled:
const results = await Promise.all([42, fetchData(), 'hello']);
// results = [42, <fetched value>, 'hello']
No automatic cancellation. When one promise rejects and all fails fast, the other promises keep running to completion — their results are just discarded. If those operations have side effects (writes, charges), you need explicit AbortController cancellation.
Unhandled promise rejection in allSettled — the silent trap. Promise.allSettled “handles” all input rejections by inspecting them — meaning unhandledrejection events never fire for the inputs. If you forget to check r.status === 'rejected' and act on the failures, those errors disappear silently:
// ❌ Silently ignores all failures — no unhandledrejection event fires
const results = await Promise.allSettled(promises);
const values = results.map(r => r.value); // r.value is undefined for rejected ones
// ✅ Always inspect status explicitly
results.forEach(r => {
if (r.status === 'rejected') console.error('Failed:', r.reason);
});
Promise.any swallows loser rejections. When one promise fulfils, the rejected promises’ reasons are gone — no unhandledrejection event for them either. Only the AggregateError fires (and only if all reject). If you need visibility into which mirrors failed, log inside each promise’s .catch.
Decision Guide
Need ALL results, and any failure means abort?
→ Promise.all
Need ALL outcomes, success AND failure, never abort?
→ Promise.allSettled
Need the FIRST result, even if it's an error (non-fetch timeouts)?
→ Promise.race (or AbortSignal.timeout for fetch)
Need the FIRST success, ignoring failures (mirrors/fallbacks)?
→ Promise.any
Need to cap parallelism (100+ requests, rate limits)?
→ Promise pool with concurrency, NOT Promise.all
Need a timeout for fetch specifically?
→ AbortSignal.timeout(ms) — or AbortSignal.any() for multiple signals
A simple mnemonic:
- all = “everyone passes or we fail”
- allSettled = “tell me how everyone did”
- race = “first across the line, dead or alive”
- any = “first one to win, ignore the losers”
Key Takeaways
- All four start their promises concurrently — the difference is when they settle and what they return
Promise.allis fail-fast — rejects the instant any promise rejects, and returns results in input order, not completion orderPromise.allSettlednever rejects — returns{status, value/reason}for each, making it the right default for partial-success scenariosPromise.racesettles on the first promise to finish, success or failure — useful for non-fetch timeouts butAbortSignal.timeout()is the modern fetch patternPromise.anysettles on the first success, ignoring rejections, only fails with anAggregateErrorwhen all reject — ideal for mirrors and fallbacksPromise.race([])hangs forever — always guard against empty arrays- None of these cancel the losing promises — pair with
AbortControllerif the losers have side effects AbortSignal.timeout(ms)is the modern fetch-timeout replacement for racing a timerAbortSignal.any([signal1, signal2])combines multiple cancellation sources (timeout + user-cancel + navigation)- TypeScript narrowing for
Promise.allSettledrequires a type predicate:.filter((r): r is PromiseFulfilledResult<T> => r.status === 'fulfilled') - For 100+ requests or rate-limited APIs, use a promise pool with concurrency cap instead of
Promise.all Promise.withResolvers()(ES2024) replaces the deferred-promise boilerplatePromise.try()(ES2025) wraps a function call so sync throws become rejectionsPromise.allSettledsilently “handles” rejections —unhandledrejectionevents never fire, so explicitly inspectr.statusto surface errorsPromise.anyswallows loser rejections — log inside each promise’s.catchif you need visibility
FAQ
What is the difference between Promise.all and Promise.allSettled?
Promise.all rejects immediately if any promise rejects, giving you nothing but the first error — use it when every result is required. Promise.allSettled waits for all promises to settle and never rejects, returning a { status, value/reason } status object for each one — use it when partial success is acceptable and you want to know which ones failed. The rule of thumb: all for “everything must succeed”, allSettled for “tell me how each one did”.
Promise.any vs Promise.race — which one when?
Promise.race settles on the first promise to finish, regardless of outcome — its main use is non-fetch timeouts where you race your operation against a timer. Promise.any settles on the first successful result and ignores failures — its main use is fetching from multiple mirrors or fallback endpoints where you take whichever responds first. Key difference: race can settle with a rejection if a promise rejects first; any ignores rejections until all fail and then throws AggregateError. For fetch timeouts specifically, use AbortSignal.timeout(ms) instead of Promise.race — it actually cancels the fetch.
Is await Promise.all([a(), b()]) faster than await a(); await b();?
Yes — sometimes by a lot. The serial version await a(); await b(); runs a() to completion, then starts b(). If each takes 1 second, total is 2 seconds. The parallel version await Promise.all([a(), b()]) starts both at the same time and waits for the slower one — total ~1 second. The key insight is that promises start eagerly, so const pa = a(); const pb = b(); await pa; await pb; is also parallel — the await only blocks for the result, not for starting the work. Use Promise.all when operations are independent (no need for one’s result to start the next).
Does Promise.all run promises in parallel?
Not exactly — JavaScript is single-threaded, so promises don’t run in parallel on the CPU. But the async operations they represent (network requests, timers, I/O) are kicked off concurrently and proceed in the background simultaneously. Promise.all lets three 300ms requests complete in ~300ms total instead of 900ms sequentially, because all three are in flight at once even though JavaScript itself runs one line at a time.
Why does my Promise.all return results in the wrong order?
It doesn’t — Promise.all always returns results in the order of the input array, not the order they completed. If you passed [slow, fast], the result is [slowValue, fastValue] even though fast finished first. This is by design and is what makes destructuring work reliably. If you need completion order, handle each promise’s resolution individually with Promise.race against the remaining set, or use for await...of on an async iterable.
What is AggregateError in Promise.any?
AggregateError is the error type Promise.any rejects with when every promise fails. It has an .errors property containing an array of all the individual rejection reasons in input order. This lets you inspect why each source failed when, for example, all your CDN mirrors are down. It is the main place AggregateError appears in everyday JavaScript. You can also construct it manually: throw new AggregateError([err1, err2], 'All retries failed').
Can I cancel the other promises when one finishes in Promise.race?
Not automatically — promises represent results, not running work, so they cannot be cancelled once started. When Promise.race settles, the losing promises keep running in the background and their results are discarded. Worse, they pin memory until they settle. To actually stop them, pass an AbortController signal to each operation and call controller.abort() once the race settles. For fetch, AbortSignal.timeout(ms) does this automatically.
Why doesn’t unhandledrejection fire when Promise.allSettled has rejected inputs?
Because Promise.allSettled inspects each input — and inspection counts as “handling” the rejection. The reasons are tucked into the { status: 'rejected', reason } shape, not propagated. If you forget to check r.status === 'rejected' and act on the failures, those errors disappear silently. Always inspect every result’s status explicitly: results.forEach(r => { if (r.status === 'rejected') console.error(r.reason); }). The same trap applies to Promise.any — the rejections of all the losing promises are swallowed once any one fulfils.
How do I limit the number of concurrent promises in JavaScript?
Use a promise pool — keep N promises in flight at once and queue the rest. The simplest version is 10 lines: spawn N worker functions that each loop pulling the next item from a shared index. The canonical library is p-limit from Sindre Sorhus: const limit = pLimit(5); await Promise.all(urls.map(url => limit(() => fetch(url)))); — at most 5 fetches in flight at any moment. Use a pool when you have 100+ requests, hit a rate-limited API, or risk socket exhaustion. For 2-10 known requests, Promise.all is fine.