Generators and Iterators
TL;DR
The iteration protocol is what for...of, spread, and destructuring rely on: an object is iterable if it has a [Symbol.iterator]() method returning an iterator (an object with next() → { value, done }). Generators (function*) are the easy way to author iterators — they pause at yield and resume on next(), giving you lazy, on-demand sequences. Async generators (async function* + for await...of) do the same for asynchronous streams. The senior value: lazy evaluation over infinite/large sequences, and streaming data without buffering it all in memory.
Interview Q&A
Q: What makes an object iterable?
A: A [Symbol.iterator] method returning an iterator. for...of calls it and then repeatedly calls next():
const range = {
from: 1, to: 3,
[Symbol.iterator]() {
let n = this.from;
const to = this.to;
return { next: () => n <= to ? { value: n++, done: false } : { value: undefined, done: true } };
},
};
[...range]; // [1, 2, 3]
for (const x of range) {} // 1, 2, 3
Arrays, strings, Map, Set, arguments, and NodeLists are built-in iterables; plain objects are not.
Q: How do generators simplify that?
A: A generator function returns an iterator that is also iterable, so you skip the boilerplate:
function* range(from, to) {
for (let n = from; n <= to; n++) yield n;
}
[...range(1, 3)]; // [1, 2, 3]
Execution pauses at each yield and resumes where it left off when next() is called — state is preserved between calls without closures-by-hand.
Q: What’s lazy evaluation good for? Show an infinite sequence.
A: Generators only compute values on demand, so you can model infinite or expensive streams and take what you need:
function* naturals() { let n = 1; while (true) yield n++; }
function* take(it, k) { for (const x of it) { if (k-- <= 0) return; yield x; } }
[...take(naturals(), 5)]; // [1, 2, 3, 4, 5] — never computes the rest
This is the basis of pull-based pipelines (map/filter/take over a generator) that never materialize the full collection.
Q: What does yield* do?
A: Delegates to another iterable, flattening it into the current generator:
function* inner() { yield 1; yield 2; }
function* outer() { yield 0; yield* inner(); yield 3; }
[...outer()]; // [0, 1, 2, 3]
Useful for composing generators and recursive traversal (e.g., walking a tree).
Q: Can you send values into a generator?
A: Yes — gen.next(value) makes that value the result of the paused yield expression. This two-way channel is the mechanism behind redux-saga and other coroutine libraries:
function* echo() {
const a = yield "ask 1"; // a = whatever next() passes in
const b = yield "ask 2";
return a + b;
}
const g = echo();
g.next(); // { value: "ask 1", done: false } — first next() can't send a value
g.next(10); // { value: "ask 2", done: false }
g.next(20); // { value: 30, done: true }
Generators also have gen.return(v) (finish early, runs finally) and gen.throw(err) (inject an error at the pause point).
Q: What are async generators and for await...of?
A: async function* yields promises; for await...of awaits each one. Ideal for paginated APIs and streams without buffering:
async function* pages(url) {
let next = url;
while (next) {
const res = await fetch(next).then(r => r.json());
yield* res.items; // stream items one page at a time
next = res.nextPage;
}
}
for await (const item of pages("/api/items")) { render(item); }
This consumes pages lazily — memory stays flat regardless of total size. Pair with AbortController to cancel mid-stream (../11_apis_data_fetching/05_abort_and_race_conditions.md).
Gotchas / edge cases
- Iterators are one-shot — once a generator is exhausted, iterating again yields nothing. Re-call the generator function for a fresh sequence.
return/breakinsidefor...ofcalls the iterator’sreturn()— your generator’sfinallyblock runs, so cleanup (closing files/streams) is reliable. Don’t put cleanup only after the lastyield.- The first
next(value)argument is ignored — there’s no pausedyieldyet to receive it. - Spreading an infinite generator hangs —
[...naturals()]never terminates; always bound it withtake. - Generators aren’t constructors —
new gen()throws. - Async iteration is sequential by default —
for await...ofawaits each item before the next; for concurrency, collect promises andPromise.allinstead.
What a senior is expected to say
- “An iterable exposes
[Symbol.iterator]; generators author iterators with pausable state viayield.” - “Generators give lazy evaluation — model infinite or huge sequences and pull only what you consume.”
- “
yield*delegates;next(v)/throw/returnmake it a two-way coroutine — that’s how saga-style flow control works.” - “Async generators +
for await...ofstream paginated/large data without buffering — flat memory.”
Cross-references
- Promises and async/await (what async generators build on): async_js/async-await.md, async_js/promises.md
- Cancelling a stream mid-flight: ../11_apis_data_fetching/05_abort_and_race_conditions.md
- redux-saga (generators in state management): ../07_state_managers/
- Backend contrast — Python generators/iterators: ../../backend/02_python_core/10_iterator_vs_generator.md
Further reading
- MDN — Iteration protocols: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
- MDN —
function*: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function* - MDN —
for await...of: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of