You build `const promises = [fetchA(), fetchB(), fetchC()]` so all three calls are already in flight, then consume it with `for await (const r of promises)`. Is that slower than `await Promise.all(promises)`, and what actually differs between the two?
answer
- promises are already-running work
- eagerness is decided at call time
- observing order is not starting order
- who attaches the rejection handler, and when
basics
~20 sWall-clock is roughly the same: the three calls were started before the loop, so both approaches finish around when the slowest one settles. What differs is delivery and failure handling — the loop yields results one at a time in array order, and a later rejection can go unhandled while the loop is still waiting on an earlier element.
solid answer
~50 sThey take about the same time here, because the array literal already started all three requests; `for await` only *observes* them one at a time, it does not start them one at a time. The real differences are three. First, delivery: the loop gives you each result as soon as it is that element's turn, so you can process incrementally, while `Promise.all` gives you nothing until every promise settles. Second, order: the loop always delivers in array order, even if element 2 settles first. Third, failure: `Promise.all` rejects on whichever promise rejects earliest and, because it subscribed to all of them up front, no rejection is left unobserved — whereas the loop only attaches a handler when it reaches an element, so a later promise that rejects while the loop is still awaiting an earlier one can trigger an unhandled-rejection event. `for await` becomes genuinely sequential only when the work is *started inside* the loop, for example iterating an array of thunks and calling each in the body.
code
javascript · 15 linesconst delay = (ms, v) => new Promise(r => setTimeout(() => r(v), ms));
async function main() {
const started = [delay(300, 1), delay(300, 2), delay(300, 3)];
let t = Date.now();
for await (const v of started) console.log(v);
console.log('already started:', Date.now() - t, 'ms'); // ~300
const thunks = [() => delay(300, 1), () => delay(300, 2), () => delay(300, 3)];
t = Date.now();
for (const make of thunks) console.log(await make());
console.log('started in the loop:', Date.now() - t, 'ms'); // ~900
}
main();go deeper
Recall that calling an async function starts it immediately, so an array of promises is already running before any loop touches it.
Explain that the loop only changes the observation order, and name the three real differences: incremental delivery, forced input order, and when rejection handlers get attached.
Show you have debugged the unhandled-rejection fallout of this pattern in a service, and describe how you keep every promise observed while still consuming results incrementally.
Own the API-shape decision: whether a component hands callers a fixed batch of promises or an async iterable determines their memory profile, their ability to abandon early, and how failures reach them.
## The trap in the question Most candidates answer "the loop is sequential, so it takes the sum of the three durations." That is wrong for the code as written, and knowing why is the point of the question. A promise in JavaScript is not a description of work to be done later — it is a handle on work **already started**. The moment `fetchA()` is evaluated inside the array literal, the request is in flight. By the time the loop runs, all three are racing in parallel. The loop cannot un-start them; it can only decide in what order to observe them. So with three 300 ms calls, both of these finish in roughly 300 ms, not 900 ms: ```js const promises = [fetchA(), fetchB(), fetchC()]; for await (const r of promises) use(r); // ~300 ms // vs const all = await Promise.all(promises); // ~300 ms ``` The sequential shape appears only when the *call itself* moves inside the loop: ```js for (const make of [() => fetchA(), () => fetchB(), () => fetchC()]) { use(await make()); // ~900 ms — each request starts after the previous finished } ``` That is the distinction worth articulating: eagerness is decided at the point the async function is **called**, not at the point its promise is awaited. ## Difference 1 — incremental vs all-or-nothing delivery `Promise.all` resolves once, with an array, after the last promise settles. Until then you have nothing to work with. The loop hands you element 0's result the moment it settles and runs your body immediately, then element 1, and so on. If your body writes rows to a database or streams output to a client, that head start matters; if you need the whole set before you can do anything, it buys you nothing. ## Difference 2 — ordering Both preserve **input order**, but differently. `Promise.all` fills a result array by index, so ordering is free and total latency is the max. The loop enforces order by *not looking* at element 1 until element 0 has settled: a fast element sitting behind a slow one is delivered late even though it finished early. Neither gives you completion order. ## Difference 3 — rejection handling, the real footgun This is the difference with production consequences. `Promise.all` attaches reactions to every promise immediately. Every rejection is therefore observed, even ones it does not report: `all` settles with the earliest rejection and quietly ignores the rest, but nothing is *unhandled*. The loop attaches a reaction only when it reaches that element. The sync-iterator fallback pulls element 1 out of the array only after element 0 has been awaited and its body has run. So consider a 500 ms element 0 and an element 1 that rejects at 20 ms: for 480 ms that rejection sits on a promise with no handler attached, and the host fires the unhandled-rejection notification — `unhandledrejection` in browsers, `process.on('unhandledRejection')` in Node, which by default terminates the process on modern Node versions. The loop later throws too, so you get both a spurious crash-level warning and the error you expected. Worse, if element 0 rejects, the loop throws immediately and elements 1 and 2 are **never pulled**, so their eventual rejections are never observed at all. The standard defence is to make sure every promise gets a handler at creation time, for example by pairing the loop with a combinator that observes all of them, or by attaching a no-op `.catch()` to each as it is created and letting the loop surface the real error. ## Choosing between them - Fixed set of independent operations, need all results, want max-not-sum latency, want fail-fast on the first error: `Promise.all`. - Values that arrive over time and you want to process each as it lands, or the number of values is unbounded: an async iterable and `for await`. - An array of promises consumed by `for await`: usually a code smell. It is `Promise.all` with worse error semantics and no latency benefit unless you truly need incremental, ordered processing. ## What the interviewer is checking That you separate *when work starts* from *when its result is observed*, and that you know awaiting in a loop does not create concurrency or remove it — it only reorders your view of it.
- How would you rewrite the code so the three requests really do run one after another?Store functions rather than promises and invoke them inside the loop: `for (const make of [() => fetchA(), () => fetchB()]) { use(await make()); }`. Nothing starts until its turn, so total time is the sum. The general rule is that deferring work in JavaScript means deferring the *call*; once a promise exists its work is already underway and no consumption pattern can hold it back.
- Concretely, why can `for await` over an array of promises produce an unhandled-rejection warning that `Promise.all` never produces?`Promise.all` subscribes to every promise up front, so every rejection has a handler from the start. The loop pulls element N out of the array only after element N-1 has been awaited, so a promise that rejects earlier than its turn sits handlerless and the host reports it. In Node that default is fatal, so the difference is not cosmetic.
- When is consuming an array of promises with `for await` actually the right call?When you need results in input order *and* want to start work on each as soon as it is available — streaming rows to a response, writing progress, updating UI per item. You accept the unhandled-rejection exposure and normally mitigate it by attaching a catch to each promise at creation. If you only need the assembled array, the combinator is simpler and safer.
saying these in an interview costs you the question
- Claims for await over started promises takes the sum of the durations
- Thinks awaiting later can prevent an already-created promise from running
- Says Promise.all runs the promises, rather than just observing them
- Assumes the loop delivers whichever promise settles first
- Unaware that an unawaited rejection is reported as unhandled