In JavaScript, what must an object provide for `for await...of` to accept it, and what happens if the object defines only `Symbol.iterator`?
answer
- two symbols, two protocols
- promise of the result object
- missing hook falls back, then throws
- iterable makes the iterator
basics
~20 sIt 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.
solid answer
~50 s`for await...of` first looks up `Symbol.asyncIterator` on the source. That method must return an async iterator — an object whose `next()` returns a promise resolving to `{ value, done }`; optional `return()` and `throw()` methods complete the protocol. If `Symbol.asyncIterator` is missing, the engine falls back to `Symbol.iterator`, gets a synchronous iterator, and wraps it in an adapter that awaits each yielded value before delivering it — that is why arrays and strings work with `for await`. If the object has neither symbol, the loop throws a `TypeError` saying the value is not async iterable. You can implement the protocol by hand on any object, which is how you adapt an event source or a paginated HTTP API into something a `for await` loop can consume, and the object is typically its own iterable by returning `this` or a fresh iterator per call.
code
javascript · 23 linesfunction ticker(count, ms) {
return {
[Symbol.asyncIterator]() {
let n = 0;
return {
next() {
if (n >= count) return Promise.resolve({ value: undefined, done: true });
n += 1;
return new Promise(res => setTimeout(() => res({ value: n, done: false }), ms));
},
return() {
console.log('iterator closed');
return Promise.resolve({ value: undefined, done: true });
},
[Symbol.asyncIterator]() { return this; },
};
},
};
}
(async () => {
for await (const tick of ticker(3, 10)) console.log(tick); // 1, 2, 3
})();go deeper
Know the loop needs a Symbol.asyncIterator method, that plain arrays and strings still work through a fallback, and that a bare object throws a TypeError.
Write the protocol out by hand: the symbol-keyed method returns an iterator whose next() resolves to { value, done }, with optional return() and throw().
Discuss adapting a real source — a paginated API, a cursor, an event emitter — and the lifecycle questions that raises: single-shot vs re-iterable, and who releases the underlying handle.
Argue about exposing async iterables as a public API surface: they give consumers pull-based flow control and early exit for free, but commit you to a per-item cost model and to defining re-iteration semantics.
## Two protocols, one loop JavaScript has a synchronous iteration protocol keyed by the well-known symbol `Symbol.iterator`, and an asynchronous one keyed by `Symbol.asyncIterator` (ES2018). They are deliberately parallel: | | sync | async | |---|---|---| | hook | `Symbol.iterator` | `Symbol.asyncIterator` | | `next()` returns | `{ value, done }` | a **promise** of `{ value, done }` | | consumed by | `for...of`, spread, destructuring | `for await...of` | The asynchronous version exists because a real async source cannot answer "is there a next value?" synchronously — finding out is the I/O. ## What the loop looks up When a `for await...of` loop begins, the engine performs an async-flavoured iterator lookup: 1. read `source[Symbol.asyncIterator]`. If it is callable, call it; the result is the async iterator and the loop drives it directly. 2. otherwise read `source[Symbol.iterator]`. If it is callable, call it and wrap the returned sync iterator in an **async-from-sync adapter**. 3. otherwise throw `TypeError: source is not async iterable`. Step 2 is why this is legal even though neither operand is an async iterable: ```js for await (const ch of 'hi') console.log(ch); // 'h', 'i' for await (const n of new Set([1, 2])) console.log(n); // 1, 2 ``` The adapter calls the sync `next()`, takes the `{ value, done }` it gets, and awaits `value` before handing it to the body. So a sync iterator that happens to yield promises behaves like an async source. ## Implementing the protocol by hand An async generator is the ergonomic way to produce one, but nothing requires it — the protocol is just methods on an object, and writing one by hand is the clearest way to show you understand it: ```js function ticker(count, ms) { return { [Symbol.asyncIterator]() { let n = 0; return { next() { if (n >= count) return Promise.resolve({ value: undefined, done: true }); n += 1; return new Promise(res => setTimeout(() => res({ value: n, done: false }), ms)); }, return() { return Promise.resolve({ value: undefined, done: true }); }, [Symbol.asyncIterator]() { return this; }, }; }, }; } for await (const tick of ticker(3, 10)) console.log(tick); // 1, 2, 3 ``` Three details in there are worth naming. **`next()` must return a promise of the result object**, not a result object containing a promise. `{ value: somePromise, done: false }` returned synchronously also happens to work for the loop (the value gets awaited), but it is not the async protocol — a consumer calling `next()` directly would get an object, not a thenable, and would break. **`done: true` ends the loop**, and the accompanying `value` is not delivered to the body. **The iterator returns itself** from `Symbol.asyncIterator`. That is convention, not requirement, but it lets a partially-consumed iterator be passed straight to another `for await` loop and resume where it left off — the same idiom the sync protocol uses. ## Iterable vs iterator Keep the two roles distinct, because interviewers probe it. The **iterable** is the object with the symbol-keyed method; calling that method produces an **iterator**, the stateful thing with `next()`. Returning a *fresh* iterator per call makes the source re-iterable (like an array); returning `this` makes it single-shot (like a generator object) — a second loop over it will find it already exhausted. Choose deliberately: a paginated API adapter that returns `this` will silently produce zero rows on a retry. ## The full method set `next(value)` is mandatory. `return(value)` is optional and is what the loop calls when you `break`, `return`, or `throw` out of it, so an iterator holding a socket, a cursor, or a file handle should implement it. `throw(err)` is optional and only matters if some consumer injects an error into the source; `for await` itself never calls it. Every one of them is expected to return a promise of a result object. ## Failure modes to recognise - `TypeError: x is not async iterable` — the object has neither symbol. A plain object of key/value pairs is the usual culprit, since objects are not iterable at all. - Using `Symbol.iterator` when you meant `Symbol.asyncIterator`: the loop still runs via the adapter, but a `next()` that returns a promise now yields a *promise-shaped result object*, and the loop sees `done` as `undefined` — an endless run of `undefined` values. - Forgetting `done: true` on the terminal step, which never ends the loop.
- What is the practical difference between returning `this` and returning a fresh iterator from `Symbol.asyncIterator`?Returning a fresh iterator makes the source re-iterable: each `for await` loop starts from the beginning, the way an array does. Returning `this` makes it single-shot — a second loop resumes on an already-drained iterator and sees nothing. For adapters over a network cursor `this` is honest, since the data really is consumed; for a source you can replay, a fresh iterator avoids a confusing empty second pass.
- If an object defines both `Symbol.asyncIterator` and `Symbol.iterator`, which one does `for await...of` use?`Symbol.asyncIterator` wins — the loop only consults `Symbol.iterator` when the async hook is absent or not callable. That lets a single object serve both loops: `for...of` gets buffered synchronous values, `for await...of` gets the streaming path. `for...of` never looks at `Symbol.asyncIterator` at all, so it would ignore the async side entirely.
- Someone's hand-written iterator has `Symbol.iterator` with a `next()` that returns a promise, and the `for await` loop spins forever on `undefined`. Why?The sync path was taken, so the adapter treats the returned promise as the result object itself. Reading `done` off a promise gives `undefined`, which is falsy, so the loop never terminates, and `value` is `undefined` too. The fix is to key the method on `Symbol.asyncIterator`, which is the hook that expects `next()` to be promise-returning.
saying these in an interview costs you the question
- Thinks only async generators can be async iterables
- Says next() should return an object containing a promise value
- Confuses the iterable (has the symbol) with the iterator (has next)
- Believes for...of will fall back to Symbol.asyncIterator
- Omits done: true and expects the loop to stop anyway