The performance question interviewers love
Two awaits in a row run sequentially. The second task does not even start until the first finishes, so two 100 ms tasks take 200 ms.
To run them in parallel, start both promises first, since calling the function starts the work, and only then await.
const p1 = fetchA(); const p2 = fetchB(); const [a, b] = await Promise.all([p1, p2]);
The comments matter more than the code here. fetchA() begins its work on the line where it is called, and await only decides when you collect the result.
Promise.all(array) returns one promise that fulfills with an array of all results, in input order, once every input fulfills.
That is the whole trick, and it is why this shows up in interviews so often. The fix for a slow endpoint is frequently three lines of reordering rather than any change in the work itself.
The same reasoning applies to a loop. Awaiting inside a for loop over ten ids is ten round trips end to end, and mapping the ids to promises first collapses them into one wait.
Sequential and parallel side by side
Four tasks, two styles, one function.
function task(name, ms) { return new Promise((resolve) => setTimeout(() => { console.log(name + " done"); resolve(name); }, ms) ); } async function main() { console.log("sequential:"); await task("A", 40); await task("B", 20); console.log("parallel:"); const c = task("C", 40); const d = task("D", 20); await Promise.all([c, d]); console.log("all finished"); } main();
Output
sequential: A done B done parallel: D done C done all finished
In the sequential half, A at 40 ms fully finishes before B at 20 ms even starts, for 60 ms total.
In the parallel half, both start together, so the shorter D finishes first and the pair costs about 40 ms rather than 60.
The completion order flipping is the visible proof that the work overlapped. Nothing about the two tasks changed, only when they were started.
const c = task("C", 40) is where C's timer begins. By the time Promise.all is called, both timers have been running for microseconds already.
Promise.all resolves to ["C", "D"] in input order even though D finished first, which the next block returns to.
all is fail-fast, and the alternatives
Four combinators cover almost every case, and picking the right one is a design decision rather than a detail.
| Combinator | Settles when | Use it for |
|---|---|---|
Promise.all | all fulfill, or one rejects | every result is required |
Promise.allSettled | all settle, never rejects | do as much as possible |
Promise.race | the first one settles, either way | timeouts |
Promise.any | the first one fulfills | first usable answer wins |
Promise.all rejects as soon as any input rejects, which is what fail-fast means. The remaining work keeps running, and its results are discarded.
allSettled gives you an array of { status, value } or { status, reason } objects, one per input, so a partial failure is reportable rather than fatal.
race settles with whichever input settles first, including a rejection, which is exactly what you want for race([work, timeoutRejection]).
The rule of thumb to say out loud in interviews is short. Use awaits in a row when step 2 needs step 1's result, and Promise.all when the tasks are independent.
a gets slowTask's result, because Promise.all always returns results in input order.
Promise.all preserves input order regardless of which promise settles first, which is what makes destructuring the result array safe.
Completion order and result order are two different things, and conflating them is the mistake this question tests.
In the earlier example the console output interleaved by completion time, and the resolved array was still ["C", "D"] in input order.
That guarantee is what lets you name the results positionally, as in const [user, orders, settings] = await Promise.all([...]).
If you genuinely need completion order, Promise.all is the wrong tool. Attaching a .then to each promise individually reacts in finish order, and allSettled still reports positionally.
The combined promise rejects at about 10 ms with that third error.
Promise.all is fail-fast, so the first rejection rejects the combined promise immediately and the other four results are discarded.
The work itself is not cancelled, which is the part people miss. Four requests keep running to completion, and nothing is listening when they finish.
That matters for side effects. If those requests write to a database, they still write, so fail-fast is about waiting rather than about stopping.
Cancelling for real takes AbortController, passing one signal to every request and calling abort() in the failure path.
Use allSettled when you want every outcome regardless of failures. It waits the full 100 ms and hands back four fulfilled entries alongside one rejected one.
Three requests in parallel
Sequential awaits would work here and take three times as long.
function fetchUser(id) { return new Promise((resolve) => setTimeout(() => resolve("user" + id), 20)); } async function main() { const users = await Promise.all([fetchUser(1), fetchUser(2), fetchUser(3)]); console.log(users.join(", ")); } main();
Output
user1, user2, user3
All three calls start inside the array literal, so the array holds three already-running promises rather than three functions to call later.
await gives back an array of the three results in input order, which is what makes users.join(", ") produce a predictable string.
Total time is about 20 ms instead of 60. The array literal is evaluated left to right in microseconds, so the three timers effectively start together.
Building the array from a list of ids scales this without repetition. Promise.all(ids.map((id) => fetchUser(id))) is the form you will actually write.
Two cautions come with that scaling. There is no .catch or try/catch here, so one rejection becomes an unhandled rejection, and mapping a thousand ids fires a thousand requests at once, which usually needs a concurrency limit.