Running work concurrently: all, allSettled, race and any
Awaiting promises one after another is right when each depends on the last — but when tasks are
independent, doing them sequentially wastes time. The promise combinators — Promise.all,
allSettled, race, any — run multiple promises concurrently, and TypeScript types each precisely,
including the subtle differences that decide which to use. Concurrency is where async bugs hide, so the
distinctions here matter.
The waste that combinators fix
Two independent fetches, done sequentially:
const user = await fetchUser(1); // takes 300ms
const orders = await fetchOrders(2); // takes 300ms — starts only AFTER user finishes
// total: ~600ms, even though neither needs the other
fetchOrders does not need user, yet it waits for fetchUser to finish first — 600ms for work that
could take 300ms. When promises are independent, you want them running at the same time. That is what
the combinators do.
Promise.all — all succeed, or the whole thing fails
Promise.all runs promises concurrently and resolves when all have succeeded, giving you a typed
tuple of their results:
const [user, orders] = await Promise.all([
fetchUser(1), // Promise<User>
fetchOrders(2), // Promise<Order[]>
]);
// user is User, orders is Order[] — both started at once, ~300ms total
Promise.all is the one you reach for most. TypeScript types it beautifully: given an array of promises
of different types, it returns a Promise<[User, Order[]]> — a tuple preserving each promise's
type in order. So destructuring [user, orders] gives each its exact type. This is a lovely use of
tuple types: Promise.all([Promise<A>, Promise<B>]) is Promise<[A, B]>.
The critical behaviour: Promise.all rejects as soon as any promise rejects. If fetchUser
fails, the whole Promise.all rejects immediately with that error, and you never get the orders (even
if it would have succeeded). This is "all or nothing" — right when you need every result and a failure
of any means you cannot proceed. But it means one failure loses all the successes, which is not always
what you want — hence allSettled.
Promise.allSettled — every result, success or failure
Promise.allSettled waits for all promises to finish — succeed or fail — and gives you a result for
each, never rejecting:
const results = await Promise.allSettled([
fetchUser(1),
fetchOrders(2),
]);
// results is an array of { status: "fulfilled"; value: T } | { status: "rejected"; reason: any }
for (const result of results) {
if (result.status === "fulfilled") {
console.log(result.value); // narrowed — value available
} else {
console.log(result.reason); // narrowed — reason (the error) available
}
}
Each result is a discriminated union (from the unions module!) — { status: "fulfilled"; value } or
{ status: "rejected"; reason } — and you narrow on status to get the value or the error. allSettled
never rejects; it always resolves with every outcome. Use it when you want all the results regardless
of individual failures — loading several independent widgets where one failing should not blank the
others, or a batch of operations where you report which succeeded and which failed. Promise.all fails
fast; allSettled collects everything.
Promise.race and Promise.any
Two more, for "the first one" cases:
// race — settles as soon as the FIRST promise settles (success OR failure)
const fastest = await Promise.race([
fetchFromServerA(),
fetchFromServerB(),
]); // whichever finishes first wins — used for timeouts
// any — resolves as soon as the first promise SUCCEEDS (ignores failures until all fail)
const firstSuccess = await Promise.any([
fetchFromMirror1(),
fetchFromMirror2(),
]); // the first success wins; rejects only if ALL fail
Promise.race settles with the first promise to settle — success or failure, whichever is first. Its
classic use is a timeout: race your operation against a delay that rejects, and whichever finishes
first wins. Promise.any settles with the first promise to succeed, ignoring failures until every one
fails — right for "try several sources, take the first that works" (mirrors, fallbacks). TypeScript types
both: Promise.race([Promise<A>, Promise<B>]) is Promise<A | B> (you get one of them), and
Promise.any similarly.
Choosing the right combinator
The choice is entirely about what you want on success and failure:
| You want… | Use |
|---|---|
| All results, and any failure should abort | Promise.all |
| All outcomes, successes and failures, never abort | Promise.allSettled |
| The first to finish (for a timeout) | Promise.race |
| The first to succeed (fallbacks/mirrors) | Promise.any |
The most common mistake is using Promise.all when you actually wanted allSettled — one failing
promise aborts everything, blanking results that succeeded. Ask: does one failure mean I cannot proceed
at all? If yes, Promise.all. If you still want the successes, allSettled. Getting this right is the
difference between a robust concurrent operation and one that falls over when any single part hiccups.
Mapping over promises
A common pattern combines map (which you know) with a combinator to run an operation over many items
concurrently:
const ids = [1, 2, 3, 4, 5];
const users = await Promise.all(ids.map((id) => fetchUser(id)));
// users is User[] — all five fetches run concurrently, results in order
ids.map(id => fetchUser(id)) produces an array of promises (Promise<User>[]), and Promise.all
runs them all concurrently and gives a typed User[]. This "map to promises, then Promise.all" is how
you fetch many things at once, and TypeScript types the result as an array of the resolved type. (For a
large number of concurrent operations you would add a concurrency limit to avoid overwhelming the
server, but the pattern is the foundation.)
Check your work
The waste combinators fix. Awaiting independent promises sequentially — they finish one after another instead of concurrently.
Promise.all. Runs promises concurrently, resolves with a typed tuple of all results — and
rejects as soon as any one rejects (all-or-nothing).
Promise.allSettled. Waits for all to finish, never rejects, gives a discriminated-union result per
promise (fulfilled/rejected) — for when you want every outcome.
Promise.race. Settles with the first promise to settle (success or failure) — for timeouts.
Promise.any. Settles with the first promise to succeed — for fallbacks/mirrors.
How TypeScript types Promise.all. As a tuple preserving each promise's type in order —
[Promise<A>, Promise<B>] → Promise<[A, B]>.
The most common combinator mistake. Using Promise.all when allSettled was wanted — one failure
aborts everything, losing the successes.
The map-to-promises pattern. Promise.all(items.map(x => asyncOp(x))) runs an operation over many
items concurrently, typed as an array of results.
Practice
- Run two independent fetches sequentially and time it; then with
Promise.alland time it. Compare. - Destructure
Promise.all([a, b])and confirm each result has its exact type (a tuple). - Make one promise in a
Promise.allreject and confirm the whole thing rejects and you lose the other result. - Use
Promise.allSettledon the same pair and confirm you get both outcomes; narrow each onstatus. - Build a timeout with
Promise.race(your operation versus a rejectingdelay). - Use
Promise.anyover two sources where one fails, and confirm the successful one wins. - Use
Promise.all(ids.map(id => fetchUser(id)))and confirm the result isUser[].
Official documentation
- MDN — Promise.all — All-or-nothing concurrency.
- MDN — Promise.allSettled — Every outcome.
- MDN — Promise.race and Promise.any — First-to-settle and first-to-succeed.
Next: typing callbacks, event emitters and streams.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship