Module 5 · Asynchronous JavaScript ⏱ 19 min

Concurrent Operations with Promise.all

By the end of this lesson you will be able to:
  • Run several independent asynchronous tasks concurrently with Promise.all and collect their results
  • Explain why the results array follows input order, not completion order
  • Choose between Promise.all and Promise.allSettled, and describe fail-fast behaviour

Not every sequence is a chain. The last lesson handled tasks that depend on each other — step B needed A's result, so they had to run in order. But plenty of work is independent: load three profiles, fetch four images, read two config files. None of them needs another's output.

Run those one after another and you pay for it in time. If each task takes a tenth of a second, three in a row cost three tenths — even though they could all have been working at once. The fix is to start them all together and wait for the group. JavaScript gives you exactly that tool in Promise.all, which turns a row of separate waits into a single combined one, so independent work finishes in the time of the slowest piece rather than the sum of all of them.

flowchart TD
  subgraph S["Sequential: one after another"]
    S1["task A"] --> S2["task B"] --> S3["task C"]
  end
  subgraph P["Concurrent with Promise.all"]
    P1["task A"] --> PA["all settle together"]
    P2["task B"] --> PA
    P3["task C"] --> PA
  end
  style S2 fill:#b91c1c,color:#fff
  style PA fill:#1e7f3d,color:#fff
Sequential work stacks end to end; concurrent work runs at once and finishes in the time of the slowest task.

How Promise.all works

Hand Promise.all an array of Promises and it returns one new Promise. That Promise waits for every input to fulfil, then resolves with an array of results — one per input, in matching positions. If the inputs resolve to 1, 2, and 3, the combined Promise resolves to [1, 2, 3].

The single most important rule about that result array is its ordering. Each result lands in the slot of the Promise that produced it: the third task may complete first, but its value still occupies index 2. You never sort the results, and you never guess which value belongs to which task — the position already tells you.

Promise.all waits for every input and resolves to an array of results, in order. Press Run.
Promise.all([Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)])
  .then((results) => console.log(results));

Order is by position, not by speed

Because independent tasks often take different amounts of time, the natural worry is that a fast result will end up in the wrong slot. It will not. Promise.all slots each result into the position of the Promise that produced it, no matter when that Promise happens to settle. A task that finishes early does not jump the queue; it waits in its own index for the rest.

This matters most when the inputs stand for different things — a user, their orders, their settings. You can hand Promise.all those three Promises in a fixed order and read the results back in that same order, confident that result 0 is always the user, never something else. Position is the contract.

flowchart LR
  I["input: [A, B, C]"] --> ALL["Promise.all"]
  F["B finishes first"] -.->|"still placed at index 1"| ALL
  ALL --> R["results: [a, b, c]<br/>(input order)"]
  style ALL fill:#3776ab,color:#fff
  style R fill:#1e7f3d,color:#fff
Results keep input order regardless of which task finishes first.
Even though B is faster, the results stay in input order: a, b, c.
function slow(v) { return new Promise((r) => setTimeout(() => r(v), 30)); }
function fast(v) { return new Promise((r) => setTimeout(() => r(v), 5)); }

Promise.all([slow("a"), fast("b"), slow("c")])
  .then((results) => console.log(results));

Failure is fast, but it does not cancel

Promise.all is fail-fast: the moment any input Promise rejects, the combined Promise rejects too, carrying that same reason. It does not wait around for the remaining inputs. That is usually what you want — if one critical load fails, there is little point collecting the others.

What it does NOT do is stop the other tasks. JavaScript Promises cannot be cancelled, so the remaining inputs keep running to completion regardless; Promise.all simply stops listening to them. You are choosing to ignore the late arrivals, not to abort them. When you genuinely need every outcome, successes and failures alike, reach for Promise.allSettled instead, which always waits and never rejects.

flowchart TD
  ALL["Promise.all"] -->|"array of results; rejects on first failure"| R1["use when every task must succeed"]
  SET["Promise.allSettled"] -->|"always waits; never rejects"| R2["use when you want every outcome"]
  RACE["Promise.race"] -->|"settles with the first to finish"| R3["use for the fastest result"]
  style ALL fill:#1e7f3d,color:#fff
  style SET fill:#3776ab,color:#fff
  style RACE fill:#b45309,color:#fff
Three combinators for three needs: all wants every success, allSettled wants every outcome, race wants the first.
Promise.allSettled waits for all and reports each outcome, success or failure.
Promise.allSettled([Promise.resolve("ok"), Promise.reject("nope")])
  .then((results) => {
    console.log(results[0].status);
    console.log(results[1].status);
  });
Exercise

What does this print? Promise.all returns a Promise you consume — not the array itself.

const p = Promise.all([Promise.resolve(1), Promise.resolve(2)]);
console.log(p instanceof Promise);
console.log(typeof p);
Exercise

Write loadAll(values) that returns a Promise resolving to an array where each input value is multiplied by 10, using Promise.all(values.map((v) => Promise.resolve(v * 10))).

function loadAll(values) {
  // return Promise.all over values.map(...)
}
Exercise

This should load every profile and return them as an array, but it uses Promise.race, which resolves with just the FIRST profile to finish — a single object, not the array. Change it to the combinator that waits for all of them.

function fetchProfile(id) { return Promise.resolve({ id: id }); }
function loadProfiles(ids) {
  return Promise.race(ids.map((id) => fetchProfile(id)));
}
Exercise

What does this print? The value of Promise.all is a Promise, not the array you passed in.

const all = Promise.all([Promise.resolve("a"), Promise.resolve("b")]);
console.log(Array.isArray(all));
console.log(typeof all.then);
Exercise

Using the provided getName(id) (which resolves to "user" + id), write loadAllNames(ids) that returns a Promise resolving to an array of names in the same order as ids, using Promise.all.

function getName(id) { return Promise.resolve("user" + id); }
function loadAllNames(ids) {
  // return Promise.all over ids.map(...)
}

Recap

  • Promise.all takes an array of Promises and resolves to an array of results, one per input.
  • Results come back in input order, not finish order — pair them by position, never by timing.
  • It is fail-fast: the first rejection rejects the whole group, but the other tasks still run to completion because Promises cannot be cancelled.
  • Use Promise.allSettled when you want every outcome, successes and failures both; it always waits and never rejects.
  • Reach for Promise.race only when the first result is all you need.

That rounds out the core of asynchronous JavaScript: the event loop and callbacks, Promises, chaining, async/await, error handling, and now concurrency. You can write non-blocking code that reads clearly, fails safely, and runs independent work in parallel.

Checkpoint quiz

Promise.all([p1, p2, p3]) resolves to:

In Promise.all([p1, p2, p3]), p2 rejects. What happens?

Go deeper — technical resources