In JavaScript, does `const a = await getA(); const b = await getB();` take the same total time as `const pa = getA(); const pb = getB(); const a = await pa; const b = await pb;`? Explain what makes them differ.
answer
- a promise is already-running work
- await consumes, it does not start
- the call site is where work begins
- hoist the calls above the awaits
- two awaits in a row is not serial
basics
~20 sNo. Calling getA() and getB() starts both operations immediately, so the second version overlaps them and costs about the slower one. Awaiting each call inline starts the second only after the first has settled, so it costs the sum.
solid answer
~50 sThey differ, and the reason is that promises in JavaScript are eager. `getA()` starts its work the moment it is called; the returned promise is a handle on something already in flight, and `await` just means "suspend until this already-running thing settles". In the first version the call to `getB()` is part of the second statement, which does not execute until `await getA()` has resumed — so the two operations never overlap and the cost is the sum. In the second version both calls happen before any await, so both are in flight together and the cost is roughly the slower of the two; the fact that you then await them one after another changes nothing. In practice prefer `const [a, b] = await Promise.all([getA(), getB()])`, which is the same concurrency plus a handler attached to both promises straight away.
go deeper
Remember that calling a promise-returning function starts the work right away, and that await only waits for it. Recognise that two independent calls can be started before either is awaited.
Explain why statement order forces serialization in the inline form: the second call is part of a continuation that has not run yet. Be able to say that awaiting two already-started promises in sequence costs the max, not the sum.
Point out the unobserved-rejection window that manual start-then-await opens, and argue for Promise.all as the default because it attaches handlers immediately and states the intent. Show how you would find such accidental serialization in a slow endpoint's timings.
Own the guidance that keeps this from recurring: what the team's default shape is for independent I/O, how latency budgets are reviewed, and when overlapping calls is the wrong answer because the downstream service would rather receive one combined request.
## A promise is work that has already started The single most useful mental correction here is that a promise is not a recipe you can choose to run later — it is a receipt for work that is already underway. Calling `getA()` runs the function body; if it is an `async` function, the body executes synchronously up to its first `await`, and by then the underlying request has usually been issued. The promise handed back is just the channel on which the eventual result will arrive. That makes `await` a *consumption* operation, not a *start* operation. It says "suspend this function and resume it when that promise settles". Nothing about it triggers the work. ## The two shapes side by side ```js // Version 1 — serialized: total ≈ tA + tB const a = await getA(); const b = await getB(); // this line has not run yet while A is in flight // Version 2 — overlapped: total ≈ max(tA, tB) const pa = getA(); // starts now const pb = getB(); // starts now, alongside A const a = await pa; const b = await pb; ``` In version 1, `getB()` is a sub-expression of the second statement. Statements run in order, and the second statement is part of the continuation scheduled when `pa` settles — so B literally cannot begin until A has finished. In version 2, both calls are evaluated before the function ever suspends, so both operations run during the same wall-clock window. The sequential-looking pair of awaits at the end of version 2 confuses people, and it is worth being explicit: awaiting `pa` and then `pb` is *not* serialization. If B takes 300 ms and A takes 100 ms, `await pa` resumes at 100 ms and `await pb` then resumes at 300 ms — total 300 ms, not 400 ms. If B finishes *first*, `await pb` returns immediately when execution reaches it, because settled promises resume their awaiters on the very next microtask. The order in which you consume the results has no effect on total elapsed time. ## Why this shows up as a real bug Code that looks like a straightforward sequence of steps is the natural way to write things, and each individual `await` looks harmless. A handler that fetches a user, their settings, and their feature flags — three independent calls of 80 ms each — quietly costs 240 ms instead of 80 ms, and nothing in the code marks it as a performance decision. The tell is the same as with a loop: latency that equals the sum of the parts. ## The rejection caveat, and why Promise.all is the better default Starting promises and awaiting them later has one sharp edge. Between `getB()` being called and `await pb` being reached, `pb` has no handler attached. If B rejects during that window — say it fails in 5 ms while A takes 2 seconds — the runtime sees a rejected promise that nobody is handling and reports an unhandled rejection, even though your code does eventually await it inside a `try`. ```js const [a, b] = await Promise.all([getA(), getB()]); ``` `Promise.all` avoids that entirely: it attaches its own reactions to every input promise at once, so there is no unobserved window, and the combined promise rejects into your `try`/`catch` normally. It also expresses the intent — "these are independent, run them together" — in a way a reader cannot misread. Reach for the manual start-then-await form only when you genuinely need to interleave other work between starting and consuming. ## What to check when you read code Ask, for each `await`, whether the expression to its right depends on a value produced by an earlier `await`. If it does not, that await is a sequencing decision you did not intend to make, and the calls can be hoisted above it and combined.
- In the start-first version, what happens if the second operation rejects while the first is still in flight?Its promise is rejected with nothing attached to it yet, so the runtime reports an unhandled rejection during that window — in Node that terminates the process by default. Your later `await pb` still throws where you expect, but the warning or crash has already fired. `Promise.all([getA(), getB()])` avoids it by attaching reactions to both promises immediately.
- If A takes 100 ms and B takes 300 ms, how long does `const a = await pa; const b = await pb;` take, and why?About 300 ms. Both were started together, so `await pa` resumes at 100 ms and execution reaches `await pb` while B is still in flight, resuming at 300 ms. Consumption order does not add time: awaiting an already-settled promise resumes on the next microtask, costing effectively nothing.
- Does an async function do anything at all before its first await?Yes — the body up to the first `await` runs synchronously, in the caller's turn, before the function returns its promise. That is why calling it issues the request. It is also why a synchronous `throw` before the first await still produces a rejected promise rather than a thrown exception at the call site.
saying these in an interview costs you the question
- The promise only starts running when you await it
- Two awaits in a row always means two round trips
- Assigning a promise to a variable defers the work
- Promise.all is required for any concurrency at all
- Awaiting a settled promise costs another full delay