Asynchronous JavaScript · Lesson 52 of 77 · concept
Sequential versus parallel waiting
About 50 minutes.
By the end of this lesson you can
- Tell "start, wait, start, wait" apart from "start both, then wait" by reading the code.
- Choose between Promise.all, allSettled, race and any from a plain-English requirement.
- State what Promise.all does with the order of its results and with the first rejection.
- Explain why "concurrent" does not mean "parallel" in JavaScript.
- Decide when awaiting inside a loop is a bug and when it is the correct answer.
Read, not run. The sandbox on this site has no promise callbacks (nothing after a .then runs), timers such as setTimeout, so the examples in this lesson are shown rather than executed. Where an example cannot run, it says so and explains why instead of leaving you to find out.
Two shapes of waiting
Explanation
Suppose three things each take one second. Written one way the whole job takes three seconds; written another way it takes one. The code differs by a couple of characters, and nothing in the syntax shouts at you about it.
The sequential shape is `const a = await first(); const b = await second();`. The second call is not made until the first answer is in hand, because `await` parked the function before the line that starts it was ever reached.
The concurrent shape is to start everything first and await afterwards: `const pending = [first(), second()]; const results = await Promise.all(pending);`. Both calls have been made by the end of the first line — calling the function is what starts the work — and only then does anything wait.
That is the whole distinction, and it hinges on one fact worth saying on its own: calling an async function starts it. Awaiting does not start anything; it only collects.
Two kettles
An analogy
You need two kettles boiled, three minutes each. Fill the first, switch it on, stand there until it clicks, then fill the second and stand there again: six minutes. Or fill both, switch both on, and wait: three minutes.
Notice what did not change. You are still one person, and you still cannot boil water with your hands. The kettles do the waiting; you only had to be smarter about when you started them.
That is precisely JavaScript's situation. Your one thread cannot do two things at once, but the network, the disk and the timers can all be waiting at the same time. Overlapping the waiting is free. Overlapping the working is not available.
Promise.all: order in, order out — and fail fast
Explanation
`Promise.all(iterable)` takes a collection of promises and returns one promise standing for all of them. It fulfils when every input fulfils, with an array of their values.
The ordering guarantee is the part people misremember, so learn it exactly: MDN states that the fulfilment value is an array of values in the order of the promises passed, regardless of completion order. The slowest request finishing last does not push its result to the end of the array. This is what makes destructuring like `const [user, orders] = await Promise.all([...])` safe.
Failure is fail-fast. If any input rejects, the returned promise rejects immediately with that first rejection reason — it does not wait for the rest. And now the part that surprises people: the other operations are not cancelled. JavaScript has no way to cancel a promise. They carry on and their results are discarded, so if they have side effects, those side effects still happen.
allSettled, race and any — pick by the question you are asking
Explanation
`Promise.allSettled` waits for every input to settle and never rejects. It fulfils with one object per input: `{ status: "fulfilled", value }` or `{ status: "rejected", reason }`. Reach for it when partial success is a real outcome — uploading twelve files and reporting which three failed.
`Promise.race` settles as soon as the first input settles, whichever way it went. A rejection can win the race. That makes it the tool for timeouts: race the real work against a promise that rejects after five seconds.
`Promise.any` fulfils with the first input that fulfils and ignores rejections, only failing if every input rejects — and then it rejects with an `AggregateError` holding all the reasons. Use it for "any of these mirrors will do".
The choice is a sentence you can say out loud. All of them, or nothing: `all`. All of them, and tell me who failed: `allSettled`. First one to finish, good or bad: `race`. First one that works: `any`.
Ask: does step B need step A's answer?
A mental model
One question decides the shape. If step B genuinely needs something step A produced — you cannot load a user's orders before you know the user's id — then sequential is not a mistake, it is the requirement. Running them concurrently is impossible, not merely unwise.
If B does not need A's answer, then awaiting A before starting B is pure waiting for no reason, and `Promise.all` is the fix.
Real code is usually a mixture: two things that can start together, then a third that needs both. Group each stage, and think in stages rather than in lines.
There is a third case people forget: independent work that you deliberately serialise anyway — because a rate limit says three requests per second, or because a hundred concurrent connections would fall over. `await` inside a `for...of` loop is the right tool there, and it is a considered choice rather than an accident.
Parallel is not always right
A common mistake
Firing everything at once. `Promise.all(tenThousandIds.map(loadOne))` opens ten thousand requests at once. The server rate-limits you, the browser queues them anyway, and one failure discards the lot. Work in batches, or use a small concurrency limit.
Using `all` where `allSettled` was meant. If you want to know which uploads failed, `all` is exactly wrong: it hands you the first rejection and throws away every successful result alongside it.
Thinking `Promise.all` makes anything faster. It does not make requests faster; it stops you waiting for them one at a time. Two 100 ms CPU-bound calculations still take 200 ms, because the single thread does both.
Assuming the losers stop. After `Promise.all` rejects or `Promise.race` settles, the other promises keep going. If they write to a database or update the screen, those effects still land — sometimes after the user has navigated away.
Recap
Recap
Calling starts the work; awaiting only collects it. Await immediately for a sequential chain, or start everything and await a combinator for concurrency. `Promise.all` returns results in input order and rejects on the first failure without cancelling anything. `allSettled` reports every outcome, `race` takes the first settlement either way, `any` takes the first success. Concurrency overlaps waiting, never computing.
Worked examples
Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.
Calling starts the work; awaiting only collects
The central fact of this lesson, demonstrated without any asynchrony at all. `start` records the moment it is called. By the time the array literal on line 8 has finished evaluating, all three have started — no waiting has happened yet.
1const started = [];2 3function start(label) {4 started.push(label);5 return { label: label };6}7 8const jobs = [start("a"), start("b"), start("c")];9 10console.log("started: " + started.join(", "));11console.log("collected: " + jobs.map((job) => job.label).join(", "));12 - line 8
Three calls, left to right, all completed before this line ends. Replace `start` with an async function and the same is true: all three are in flight.
- line 11
Collecting the results is a separate step. In real code this line is the `await Promise.all(jobs)` — and it is the only line that waits.
What it prints
started: a, b, ccollected: a, b, c
The same three seconds, or one
Two functions doing identical work. The only difference is where the `await` goes — and that difference is the whole lesson. Assume each `load` takes one second.
1// Sequential: about 3 seconds. Each call waits for the previous answer.2async function loadSequential() {3 const users = await loadUsers();4 const orders = await loadOrders();5 const products = await loadProducts();6 return { users, orders, products };7}8 9// Concurrent: about 1 second. All three start before anything waits.10async function loadConcurrent() {11 const usersPending = loadUsers();12 const ordersPending = loadOrders();13 const productsPending = loadProducts();14 const [users, orders, products] = await Promise.all([15 usersPending,16 ordersPending,17 productsPending,18 ]);19 return { users, orders, products };20}21 22// Genuinely sequential: the second call NEEDS the first answer.23async function loadUserOrders(id) {24 const user = await loadUser(id);25 return loadOrders(user.accountId);26}27 - line 4
`loadOrders` has not been called yet when line 3 starts waiting. That is where the extra seconds go.
- line 11
No `await` here, so this line only starts the work and moves on. Three lines later, all three are in flight.
- line 14
Destructuring in input order is safe: Promise.all fulfils with values in the order of the promises passed, regardless of which finished first.
- line 24
This one is correctly sequential. `user.accountId` does not exist until the first call has answered, so there is nothing to overlap.
This one does not run hereThe three loaders are asynchronous stand-ins that do not exist here, and without a job-queue drain neither function could get past its first `await`.
The four combinators, side by side
One scenario per combinator, chosen so that swapping any of them for another would be a real bug. Read the comment above each — that sentence is the requirement the combinator was picked for.
1// All of them, or nothing. One failure discards the rest.2const [user, settings] = await Promise.all([loadUser(id), loadSettings(id)]);3 4// All of them, and tell me who failed. This one never rejects.5const outcomes = await Promise.allSettled(files.map(upload));6const failed = outcomes.filter(function (outcome) {7 return outcome.status === "rejected";8});9console.log(failed.length + " of " + outcomes.length + " uploads failed");10 11// First to settle, good or bad. A rejection can win, which is the point here.12const report = await Promise.race([loadReport(id), rejectAfter(5000)]);13 14// First one that WORKS. Rejections are ignored unless every mirror fails,15// and then it rejects with an AggregateError holding all the reasons.16const config = await Promise.any([fromCache(), fromDisk(), fromNetwork()]);17 - line 5
`allSettled` gives one object per input: `{ status: "fulfilled", value }` or `{ status: "rejected", reason }`. Using `all` here would throw away the successful uploads along with the failure.
- line 12
The timeout idiom. `rejectAfter` loses the race when the report arrives first — but it is not cancelled, and the real request is not cancelled either if the timeout wins.
- line 16
`any` skips past failures. If the cache and disk both reject, the network answer still fulfils the whole expression.
This one does not run hereThese are top-level `await` expressions over asynchronous stand-ins. Top-level `await` is a syntax error in this sandbox, and no handler could resume anyway.
Check yourself
Not scored, not stored. Getting one wrong is the useful part.
`begin` records each call. Before running it, predict both lines — in particular, has anything been "collected" before line 8 finishes?
const started = [];function begin(label) { started.push(label); return label.toUpperCase();}const results = [begin("a"), begin("b")];console.log(started.join(","));console.log(results.join(","));
`Promise.all([slow, fast])` where `slow` takes 2 s and `fast` takes 10 ms. What does it fulfil with?
You upload twelve files and need to report which ones failed while keeping the ones that worked. Which combinator?
`Promise.all([a, b, c])` rejects because `b` failed after 100 ms. What happens to `a` and `c`?
No exercise in this lesson
The difference between sequential and concurrent waiting is only observable in elapsed time and settlement order, and this sandbox has neither timers nor a drained job queue (spec items RP-1, RP-2). Nothing gradable would remain.
Worth remembering
- What starts asynchronous work — calling the function, or awaiting it?
Calling it. `await` only collects a result that is already on its way. That is why `const a = f(); const b = g(); await Promise.all([a, b]);` overlaps the waiting while `await f(); await g();` does not.
- State Promise.all's two guarantees.
Its fulfilment array is in the order the promises were passed, regardless of completion order; and it rejects immediately with the first rejection reason. It does not cancel the others — they run on and their results are discarded.
- all, allSettled, race, any — one sentence each.
all: all of them or nothing. allSettled: all of them, and tell me who failed (never rejects). race: first to settle, success or failure. any: first that succeeds, rejecting with an AggregateError only if every input fails.
Check this lesson against the source
We wrote the explanation; we did not invent the facts. This is the page that backs them.
Check there: That the fulfilment value is an array of values in the order of the promises passed, regardless of completion order; and that it rejects immediately when any input rejects, with that first rejection reason.
Check there: That it fulfils once all inputs settle, with objects carrying status and either value or reason, and that it never rejects.
Check there: That it fulfils with the first fulfilled input, and rejects with an AggregateError only when every input rejects.
Check there: That it settles with the first input to settle, whether that is a fulfilment or a rejection.
Prefer to read the source in a structured breakdown? Turn this doc into a breakdown