skip to content

for await...of and Async Iterables

for await...of pulls values from an async iterable one at a time, awaiting each before the next. Interviewers ask how it differs from awaiting an array of promises, since one is a sequential pull and the other an eager fan-out.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

4

In JavaScript, what does a `for await (const value of source)` loop do that a plain `for...of` loop does not, and what kinds of source can it consume?

level: juniorimportance: should knowfreq 58%

answer

  1. asynchronous cousin of for...of
  2. awaits the value each turn
  3. two protocols, one falls back
  4. only where await is legal

basics

~20 s

for await...of awaits every step of the iteration. It consumes async iterables (objects with a Symbol.asyncIterator method) and also ordinary iterables, awaiting each produced value before the body runs, so the body sees settled values instead of promises.

solid answer

~50 s

`for await...of` is the asynchronous form of `for...of`, added in ES2018. On each turn it calls the iterator's `next()`, awaits the result object, and awaits the `value` inside it, so the loop body always receives a settled value rather than a promise. It accepts two kinds of source. If the object has a `Symbol.asyncIterator` method, that async iterator drives the loop. If it does not, the engine falls back to `Symbol.iterator` and wraps the sync iterator so each yielded value is awaited — which is why `for await` over a plain array of promises hands you the resolved values in array order. If any awaited value rejects, the loop throws at that point, so a `try/catch` around the loop catches it. The construct is only legal where `await` is legal: inside an async function, an async generator, or at the top level of an ES module.

go deeper

for a junior

Be able to say that the loop awaits each value for you, works on async iterables and on plain iterables of promises, and only compiles where await is allowed.

for a middle

Explain the two lookup paths — Symbol.asyncIterator first, then a wrapped Symbol.iterator — and that the loop awaits both the result object from next() and the value inside it.

for a senior

Show judgment about when ordered one-at-a-time consumption is the right shape at all, and be ready to say what it costs in latency versus overlapping the work.

for a principal

Frame it as an API-design choice: exposing a source as an async iterable gives consumers natural pull-based flow control, while exposing a promise of an array forces the whole result into memory and forfeits incremental delivery.

## The one-sentence version `for await...of` is `for...of` with an `await` inserted at every point where the iteration protocol could produce a promise. It was added in ES2018 alongside async generators and the `Symbol.asyncIterator` well-known symbol. ## What one turn of the loop does A synchronous `for...of` loop calls `iterator.next()` and expects an object shaped `{ value, done }` back immediately. An asynchronous source cannot do that: it does not yet know whether there is a next value, because finding out requires I/O. So the async iteration protocol says `next()` returns a **promise** of `{ value, done }`. `for await...of` therefore does, per turn: 1. call `next()` on the iterator; 2. `await` what comes back, to get the `{ value, done }` object; 3. if `done` is true, exit the loop; 4. otherwise bind `value` to the loop variable and run the body; 5. repeat. Because step 2 is a real `await`, the loop **suspends the enclosing async function** between turns and yields to the event loop. Nothing after the loop runs until the source is exhausted. ## Where it is allowed The loop contains an implicit `await`, so it is subject to exactly the same placement rule as `await` itself: it is legal inside an `async function`, inside an `async function*`, and at the top level of an ES module (top-level await). Writing it in a plain synchronous function is a **syntax error**, not a runtime error — a common surprise when someone drops one into an array `forEach` callback, which is an ordinary non-async function. ## The two kinds of source When the loop starts, the engine looks for a `Symbol.asyncIterator` method on the source and calls it to get an async iterator. That is the primary path. If `Symbol.asyncIterator` is absent, the engine falls back to `Symbol.iterator` and wraps the resulting synchronous iterator in an adapter (the spec calls it an async-from-sync iterator). The adapter calls the sync `next()`, then awaits the `value` it produced before delivering it. That fallback is the reason this works: ```js const delay = (ms, v) => new Promise(r => setTimeout(() => r(v), ms)); async function run() { for await (const v of [delay(30, 'a'), delay(10, 'b'), 'c']) { console.log(v); // 'a', then 'b', then 'c' } } ``` An array is not an async iterable — it only has `Symbol.iterator` — yet the loop still prints resolved strings, in **array order**, not completion order. Non-promise values such as `'c'` pass through untouched, because awaiting a non-promise simply resolves to it after a microtask. ## Contrast with plain for...of Write the same array with `for...of` and each `v` is the promise object itself: ```js for (const v of [delay(30, 'a')]) { console.log(v); // Promise { <pending> } } ``` You would have to `await v` yourself inside the body. Note that this manual form is *not* equivalent to `for await` in one respect: `for await` also awaits the **result object** from `next()`, which is what makes genuinely asynchronous sources (ones that do not know their next value yet) work at all. An array always knows. ## Errors If `next()` rejects, or the value it produced rejects, the `await` throws inside the loop, the loop stops, and the exception propagates out of the loop like any other. So the natural shape is: ```js try { for await (const chunk of source) process(chunk); } catch (err) { // first failure that surfaced during iteration } ``` A rejection also terminates iteration — the loop does not skip the bad value and continue. If you want to keep going, the `try/catch` has to be *inside* the body around the work you do, and the source itself must not be the thing that failed. ## What it does not do It does not add concurrency. Each turn waits for the previous one. That is precisely its purpose — a sequential pull, one value at a time — and it is the opposite of a fan-out combinator that starts everything at once. Reach for it when values arrive over time (streamed chunks, pages of an API, events) and you want to handle each as it comes; reach for a fan-out when you have a fixed set of independent operations and want them overlapped.

  • Why is `for await...of` a syntax error inside a plain callback passed to `array.forEach`?
    Because the loop contains an implicit `await`, and `await` is only legal inside an async function, an async generator, or a module's top level. A `forEach` callback is an ordinary function, so the parser rejects the loop outright. Marking the callback `async` fixes the syntax, but then `forEach` ignores the returned promise and does not wait for it — which is usually the real bug.
  • If one value in the middle of the iteration rejects, does the loop skip it and continue?
    No. The rejection surfaces at the implicit `await`, so it throws out of the loop and iteration stops there. Values that would have come later are never requested. To survive a bad item you have to wrap the work inside the body in `try/catch`, and even that only helps if the failure came from your processing rather than from the source producing the value.
  • Does `for await` over an array of promises give you values in completion order?
    No — in array order. The sync-iterator fallback pulls element 0, awaits it, delivers it, then pulls element 1. A fast promise sitting at index 2 still waits its turn behind a slow one at index 0. If you want completion order you need a different mechanism entirely; the loop's whole contract is ordered, one-at-a-time delivery.

saying these in an interview costs you the question

  • Thinks for await...of runs the iterations in parallel
  • Says it only works on async generators, not arrays
  • Believes the loop body receives promises you must await yourself
  • Thinks a rejected value is skipped and the loop continues
  • Writes for await inside a non-async callback and expects it to parse

context

open as a page

In JavaScript, what must an object provide for `for await...of` to accept it, and what happens if the object defines only `Symbol.iterator`?

level: middleimportance: should knowfreq 44%

basics

~20 s

It needs a Symbol.asyncIterator method returning an object with a next() that produces a promise of { value, done }. If only Symbol.iterator exists, the engine wraps that sync iterator and awaits each value; if neither exists, the loop throws a TypeError.

open as a page

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?

level: middleimportance: should knowfreq 52%

basics

~20 s

Wall-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.

open as a page

In JavaScript, what does breaking out of a `for await...of` loop early do to the iterator it was consuming, and why can that still leave a connection or file handle open?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

Leaving the loop early calls the iterator's optional return() method and awaits it, giving the source a chance to clean up. If the iterator does not implement return(), nothing is notified and whatever it holds open stays open — the loop cannot clean up a resource it does not know about.

open as a page