RizTech Academy logo
RizTech Academy
Asynchronous TypeScriptLesson 3 of 530 min

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

  1. Run two independent fetches sequentially and time it; then with Promise.all and time it. Compare.
  2. Destructure Promise.all([a, b]) and confirm each result has its exact type (a tuple).
  3. Make one promise in a Promise.all reject and confirm the whole thing rejects and you lose the other result.
  4. Use Promise.allSettled on the same pair and confirm you get both outcomes; narrow each on status.
  5. Build a timeout with Promise.race (your operation versus a rejecting delay).
  6. Use Promise.any over two sources where one fails, and confirm the successful one wins.
  7. Use Promise.all(ids.map(id => fetchUser(id))) and confirm the result is User[].

Official documentation

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