frontend / javascript core / generators-and-iterators.md

Generators and Iterators

4 min read source

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/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 constructorsnew gen() throws.
  • Async iteration is sequential by defaultfor await...of awaits each item before the next; for concurrency, collect promises and Promise.all instead.

What a senior is expected to say

  • “An iterable exposes [Symbol.iterator]; generators author iterators with pausable state via yield.”
  • “Generators give lazy evaluation — model infinite or huge sequences and pull only what you consume.”
  • yield* delegates; next(v)/throw/return make it a two-way coroutine — that’s how saga-style flow control works.”
  • “Async generators + for await...of stream paginated/large data without buffering — flat memory.”

Cross-references

Further reading