Frontend / JavaScript core / 11_generators_and_iterators.md

Generators and Iterators

Updated 4 interview angles 5 min read source
On this page10
  1. What makes an object iterable?
  2. How do generators simplify that?
  3. What’s lazy evaluation good for? Show an infinite sequence.
  4. What does yield* do?
  5. Can you send values into a generator?
  6. What are async generators and for await...of?
  7. Gotchas
  8. Related
  9. Further reading
  10. Interview angle

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():

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

How do generators simplify that?

A generator function returns an iterator that is also iterable, so you skip the boilerplate:

js
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:

js
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:

js
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:

js
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:

js
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/break inside for...of calls the iterator’s return() — your generator’s finally block runs, so cleanup (closing files/streams) is reliable. Don’t put cleanup only after the last yield.
  • The first next(value) argument is ignored — there’s no paused yield yet to receive it.
  • Spreading an infinite generator hangs — [...naturals()] never terminates; always bound it with take.
  • Generators aren’t constructors — new gen() throws.
  • Async iteration is sequential by default — for await...of awaits each item before the next; for concurrency, collect promises and Promise.all instead.

Further reading

Interview angle 4

  • “What makes something iterable?” - a [Symbol.iterator] method returning an object with next(). That protocol is what for...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 yield pauses 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.
  • “Generators are described as two-way. What does that mean?” - next(v) sends a value into the paused generator as the result of its yield, and throw() and return() 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.