Generators and Iterators
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.
What makes an object iterable?
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, 3Arrays, strings, Map, Set, arguments, and NodeLists are built-in iterables; plain objects are not.
How do generators simplify that?
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.
What’s lazy evaluation good for? Show an infinite sequence.
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; } }
// [1, 2, 3, 4, 5] — never computes the rest
[...take(naturals(), 5)];This is the basis of pull-based pipelines (map/filter/take over a generator) that never materialize the full collection.
What does yield* do?
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).
Can you send values into a generator?
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() {
// a = whatever next() passes in
const a = yield "ask 1";
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).
What are async generators and for await...of?
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());
// stream items one page at a time
yield* res.items;
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 (Race conditions and AbortController).
Gotchas
- 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.
Related
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
Interview angle 4
- “What makes something iterable?” - a
[Symbol.iterator]method returning an object withnext(). That protocol is whatfor...of, spread and destructuring all consume, so implementing it makes your type work everywhere an array does. - “What do generators add over writing that by hand?” - they let you author
an iterator as ordinary sequential code, because
yieldpauses the function and keeps the local state. The alternative is a hand-rolled state machine, and a generator is the same thing with the bookkeeping generated for you. - “Where does laziness actually pay?” - anywhere the sequence is huge or
unbounded. A generator over an infinite series or a million-row file computes
only what the consumer pulls, so memory stays flat instead of scaling with the
source.
- Follow-up: “How would you consume a paginated API that way?” - an async
generator with
for await...of. Each page is fetched as the consumer asks for it, so you stream rather than buffering the whole result set, and the consumer’s loop looks like it is reading a list.
- Follow-up: “How would you consume a paginated API that way?” - an async
generator with
- “Generators are described as two-way. What does that mean?” -
next(v)sends a value into the paused generator as the result of itsyield, andthrow()andreturn()inject an error or an early exit at that point. That is what makes saga-style flow control work: the generator declares what it wants and a driver decides what to hand back.