frontend / es features / 04_iterators_and_generators.md

Iterators and Generators (incl. for-await-of)

6 min read source

Iterators and Generators (incl. for-await-of)

TL;DR

An iterator is an object with a next() method that returns { value, done }. The iterable protocol says “this object has a [Symbol.iterator] method that returns an iterator.” Anything with that protocol works with for...of, spread, destructuring. Generators (function* + yield) are a syntax for creating iterators without writing the next() plumbing. Async iterators + for await...of handle streams (Node streams, ReadableStream, paginated APIs). The newer iterator helpers (ES2025) add map/filter/take/reduce directly on iterators — like array methods, but lazy.

Interview Q&A

Q: Iterable vs iterator — what’s the distinction?

A:

  • Iterable: an object with [Symbol.iterator]() that returns an iterator.
  • Iterator: an object with next() returning { value, done }.

Most built-ins (Array, Map, Set, String, NodeList, generators) are iterable. Calling obj[Symbol.iterator]() gives you the iterator.

const arr = [1, 2, 3];
const it = arr[Symbol.iterator]();
it.next();   // { value: 1, done: false }
it.next();   // { value: 2, done: false }
it.next();   // { value: 3, done: false }
it.next();   // { value: undefined, done: true }

Q: Custom iterable — show me.

A:

class Range {
  constructor(public start: number, public end: number) {}

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;
    return {
      next: () => current < end
        ? { value: current++, done: false }
        : { value: undefined, done: true },
    };
  }
}

for (const n of new Range(1, 5)) console.log(n);   // 1, 2, 3, 4
[...new Range(0, 3)];                                // [0, 1, 2]
const [a, b] = new Range(10, 20);                    // a=10, b=11

Hand-rolling iterators is rare — use generators instead.

Q: Generators — what they buy you.

A: Same iterable + iterator, much less code:

function* range(start: number, end: number) {
  for (let i = start; i < end; i++) yield i;
}

for (const n of range(1, 5)) console.log(n);
[...range(0, 3)];                                    // [0, 1, 2]

function* creates a generator function; calling it returns a generator object (which is both iterable and iterator). yield pauses execution; next() resumes.

Q: yield and yield*.

A:

function* outer() {
  yield 1;
  yield 2;
  yield* inner();    // delegate to another iterable
  yield 5;
}

function* inner() {
  yield 3;
  yield 4;
}

[...outer()];   // [1, 2, 3, 4, 5]

yield* flattens another iterable into the current generator. Useful for composing generators or yielding through built-ins (yield* arr).

Q: When use generators?

A: Lazy sequences:

  • Infinite seriesfunction* naturals() { let i = 0; while (true) yield i++; }.
  • Streaming parsers — yield tokens as you parse a string.
  • Tree traversal — yield nodes in order without building an array.
  • Custom iteration for domain objects.

Avoid generators where simple array methods work — they’re slower and harder to read for short cases.

Q: Async iterators and for await...of.

A: Async iterators have [Symbol.asyncIterator]() returning an iterator whose next() returns a Promise<{ value, done }>. Use for await...of:

async function* fetchAllPages(url: string) {
  let cursor: string | undefined;
  do {
    const res = await fetch(`${url}?cursor=${cursor ?? ""}`);
    const { items, nextCursor } = await res.json();
    for (const item of items) yield item;
    cursor = nextCursor;
  } while (cursor);
}

for await (const item of fetchAllPages("/api/users")) {
  console.log(item);
}

Each iteration fetches the next page; consumer doesn’t see the pagination. Same pattern for streams, message queues, SSE.

async function* creates async generators — natural syntax for streamed work.

Q: Native async iterators in browser / Node.

A:

  • Node streamsfs.createReadStream etc. implement async iteration:
    for await (const chunk of fs.createReadStream("big.txt", "utf-8")) {
      process(chunk);
    }
  • fetch body (browser) — response.body is a ReadableStream; async iteration works:
    for await (const chunk of response.body!) {
      process(chunk);
    }
  • Web events — Web Streams API + helpers.

For SSE consumption with POST (where EventSource can’t be used), for await over response.body is the natural pattern (see ../11_apis_data_fetching/08_server_sent_events.md).

Q: Iterator helpers — ES2025.

A: Methods directly on iterators, lazy by default:

function* naturals() { let i = 1; while (true) yield i++; }

const result = naturals()
  .filter(n => n % 2 === 0)
  .map(n => n * 10)
  .take(5)
  .toArray();
// [20, 40, 60, 80, 100]

Available methods: map, filter, take, drop, flatMap, reduce, toArray, forEach, some, every, find.

Key difference from array methods: lazy. .filter().map() doesn’t iterate the whole sequence — it pipelines. Perfect for infinite or large sequences where you only need a slice.

Available in Chrome/Edge 122+, Firefox 131+, Safari 18.4+, Node 22+. Polyfill via core-js or iterator-helpers-polyfill.

Q: Generators for cancellation / control flow.

A: Generators predate async/await and were the foundation for libraries like co.js + Redux Saga. The cooperative pause/resume is still useful for:

  • State machines — each yield is a state.
  • Cancellable async — Saga’s cancel() works because generators can be aborted at yield.
  • Coroutines — multiple generators sharing control.

For routine async, async/await is simpler. Reach for generators when the control flow is genuinely about pausing/resuming.

Q: return and throw on iterators.

A: Iterators may implement return(value) (early termination) and throw(error) (re-raise inside the iterator’s body):

function* counter() {
  try {
    let i = 0;
    while (true) {
      yield i++;
    }
  } finally {
    console.log("cleanup");
  }
}

const it = counter();
it.next();      // 0
it.next();      // 1
it.return(99);  // cleanup logged, returns { value: 99, done: true }

for...of calls return() when breaking early (loop body break or throw). Use the finally in a generator for cleanup — for...of ensures it runs.

Gotchas / edge cases

  • Generators are iterators and iterablesgen[Symbol.iterator]() returns gen itself. So you can both iterate them and call next() directly.
  • Iterators are statefulfor...of on the same iterator twice yields nothing the second time (already done).
  • Spread [...obj] requires an iterable[...{}] throws “not iterable” because plain objects aren’t iterable by default. Use Object.entries(obj) to iterate.
  • for...in is for object keys; for...of is for iterables. Easy to mix up.
  • Async iterators in browsers were spotty until ~2022; modern is fine.
  • Generators inside React render — don’t. React renders are pure; calling a generator function in render creates new state. Use in effects/handlers.
  • TypeScript types — generator types: Generator<TYield, TReturn, TNext>. Most cases only care about TYield.

What a senior is expected to say

  • “Iterable + iterator protocol is what for...of, spread, destructuring depend on. Generators are the syntax for writing iterators concisely.”
  • “Async generators + for await...of for streamed data — pagination, server-sent events, file streams. Hides the iteration mechanics.”
  • “Iterator helpers (ES2025) bring lazy map/filter/take directly on iterators. Pipelines large or infinite sequences without materializing arrays.”
  • “Generators for cooperative pausing/resuming — state machines, Redux Sagas. For routine async, async/await is simpler.”
  • for...of calls return() on the iterator on early break — useful for cleanup in finally.”

Cross-references

Further reading