Generators & Async Iterators
โฑ๏ธ ~4-minute bite ยท solve the sandbox to master
5-Year-Old Metaphor
โ The physical, real-world picture. No jargon.๐ A generator is a book with a bookmark โ each yield places the bookmark and hands you the page. .next() opens at the bookmark and keeps reading.
The bookmark analogy
A normal function is a book you read start to finish in one sitting โ when it returns, you close the book and forget your place. A generator is a book with a physical bookmark. You read until you hit a yield, the bookmark goes in, and the function hands you the current page. Next time you call .next(), the book opens exactly at the bookmark โ same chapter, same paragraph, same local variables โ and reading continues from there.
The bookmark is not a copy of the page. It's a reference into the live book. The story continues from exactly where it left off, and any variables that were in scope are still there โ because the function's stack frame is kept alive on the heap rather than discarded.
| 1 | function* book() { |
| 2 | console.log("Chapter 1"); // runs on first next() |
| 3 | yield "end of chapter 1"; // bookmark placed here |
| 4 | console.log("Chapter 2"); // runs on second next() |
| 5 | yield "end of chapter 2"; |
| 6 | console.log("The End"); // runs on third next() |
| 7 | // implicit return โ { value: undefined, done: true } |
| 8 | } |
| 9 | const reader = book(); |
| 10 | reader.next(); // logs "Chapter 1" โ { value: "end of chapter 1", done: false } |
| 11 | reader.next(); // logs "Chapter 2" โ { value: "end of chapter 2", done: false } |
| 12 | reader.next(); // logs "The End" โ { value: undefined, done: true } |
Generator as suspended coroutine โ the four states
The ECMAScript spec tracks each GeneratorObject through four internal states. Understanding these states is the difference between guessing and knowing exactly what a generator will do:
suspendedStart
Just created via gen(). The function body hasn't run yet. Waiting for the first next() call.
executing
next() was called. The function body is running right now. Lasts until the next yield or return.
suspendedYield
Hit a yield expression. Stack frame preserved on heap. Waiting for the next next() call.
completed
Function returned (or threw). No more values. All subsequent next() calls return { value: undefined, done: true }.
Lazy vs eager โ why generators save memory
An array is eager: it computes and stores every element immediately, regardless of how many you'll actually use. A generator is lazy: it computes the next element only when asked. For large or infinite sequences, this difference is enormous.
Eager โ array (O(n) memory)
| 1 | // Must hold all 1M numbers at once |
| 2 | const nums = Array.from( |
| 3 | { length: 1_000_000 }, |
| 4 | (_, i) => i |
| 5 | ); |
| 6 | // ~4 MB in memory immediately |
Lazy โ generator (O(1) memory)
| 1 | function* naturals() { |
| 2 | let n = 0; |
| 3 | while (true) yield n++; |
| 4 | } |
| 5 | // Infinite sequence. |
| 6 | // Holds exactly one number at a time. |
Iterator protocol โ why generators work with for...of automatically
Generators implement both the Iterable protocol (they have a [Symbol.iterator]() method that returns themselves) and the Iterator protocol (they have a next() method that returns { value, done }). This means you get all of these for free:
| 1 | function* range(n) { |
| 2 | for (let i = 0; i < n; i++) yield i; |
| 3 | } |
| 4 | ย |
| 5 | // All of these work out of the box: |
| 6 | for (const x of range(3)) { } // for...of |
| 7 | const arr = [...range(3)]; // spread โ [0, 1, 2] |
| 8 | const [a, b] = range(3); // destructuring โ 0, 1 |
| 9 | const set = new Set(range(5)); // constructors that accept iterables |
Interactive Sandbox
โ Move something, see it react instantly.Pattern
Code
const gen = counter(0);Generator state
not startedLast returned
โLocal variables
Call next() to startCall trace
Challenge
Explore all 5 generator patterns. Pay special attention to two-way communication โ why the first next() value is discarded trips up even experienced developers.
Why Should I Care?
โ The exact interview question + the bug it kills.Core interview questions
Q: What makes generators lazy? How are they different from an array?
Generators compute values on demand โ the function body only runs when next() is called. An array is eager: all values are computed and held in memory immediately. For a sequence of 1 million items, an array requires O(n) memory; a generator requires O(1). For infinite sequences, an array is impossible; a generator is trivial.
| 1 | // Generator: take first 3 from infinite stream โ O(1) memory |
| 2 | function* naturals() { let n = 0; while (true) yield n++; } |
| 3 | const first3 = [...take(naturals(), 3)]; // [0, 1, 2] |
| 4 | ย |
| 5 | // Array: impossible to represent an infinite sequence |
Q: How does two-way communication work with next(value)? What does the first next() return?
The value passed to next(v) becomes the result of the yield expression inside the generator (i.e., const x = yield something โ x receives v). The first next() must be called without a value because there's no yield expression waiting to receive it yet โ the generator hasn't started running. Any value passed to the first next() is silently discarded.
| 1 | function* echo() { |
| 2 | while (true) { |
| 3 | const received = yield "ready"; |
| 4 | console.log("Got:", received); |
| 5 | } |
| 6 | } |
| 7 | const g = echo(); |
| 8 | g.next(); // โ first call: starts the generator, runs until yield, returns { value: "ready" } |
| 9 | g.next("hello"); // โ "Got: hello", returns { value: "ready" } |
| 10 | g.next("world"); // โ "Got: world", returns { value: "ready" } |
Q: What is yield* and when do you use it?
yield* delegates iteration to another iterable (generator, array, string, Map, etc.). It transparently forwards every next(), return(), and throw() call through to the delegated iterator. From the caller's perspective, delegation is invisible โ they see a flat stream of values. Use it when you want to compose generators, flatten nested iterables, or split a large generator into smaller reusable pieces.
Q: How do async generators differ from regular generators?
Regular generators: next() returns { value, done } synchronously. Async generators (async function*): next() returns a Promise<{ value, done }>. Inside an async generator, you can use both yield and await. They implement [Symbol.asyncIterator] instead of [Symbol.iterator] โ consumed with for await...of, not plain for...of.
Production use cases
Infinite data streams โ pagination, event streams
API pagination is a perfect async generator use case: each page is fetched only when the consumer asks for the next chunk, no intermediate arrays are built, and the consumer can stop at any time by breaking out of for await...of.
| 1 | async function* fetchPages(url) { |
| 2 | while (url) { |
| 3 | const res = await fetch(url); |
| 4 | const { data, nextUrl } = await res.json(); |
| 5 | yield* data; // yield each item individually |
| 6 | url = nextUrl; // follow cursor until null |
| 7 | } |
| 8 | } |
| 9 | ย |
| 10 | for await (const item of fetchPages("/api/items?page=1")) { |
| 11 | process(item); |
| 12 | if (shouldStop()) break; // gen.return() called โ cleans up automatically |
| 13 | } |
Redux-Saga โ why generators enable cancellable side effects
Redux-Saga uses generators because a suspended generator is trivially cancellable: just stop calling next(). The saga middleware drives each generator by calling next(result) after each async effect resolves. If the saga is cancelled mid-way (component unmounts, user navigates away), the middleware simply stops, and the generator's suspended stack frame is garbage collected. No cleanup needed.
| 1 | // Redux-Saga uses generators exactly like this: |
| 2 | function* fetchUserSaga(action) { |
| 3 | try { |
| 4 | const user = yield call(api.fetchUser, action.id); // โ suspends here |
| 5 | yield put({ type: "FETCH_SUCCESS", user }); // โ and here |
| 6 | } catch (e) { |
| 7 | yield put({ type: "FETCH_FAILED", error: e }); |
| 8 | } |
| 9 | } |
| 10 | // If the saga is cancelled between yields, the generator |
| 11 | // is simply never resumed โ no AbortController needed. |
The Deep Dive
โ Spec refs, engine internals, the minutiae.ES spec: GeneratorObject internal states
The ECMAScript specification defines GeneratorObject as having an internal slot [[GeneratorState]] with exactly four possible values. Every call to next(), return(), or throw() checks this slot and transitions accordingly:
| 1 | // Simplified spec pseudocode for GeneratorResume: |
| 2 | function GeneratorResume(generator, value, generatorBrand) { |
| 3 | const state = generator.[[GeneratorState]]; |
| 4 | if (state === "completed") { |
| 5 | return { value: undefined, done: true }; // exhausted โ always |
| 6 | } |
| 7 | if (state === "executing") { |
| 8 | throw new TypeError("Generator is already running"); // reentrant guard |
| 9 | } |
| 10 | // state is "suspendedStart" or "suspendedYield" |
| 11 | generator.[[GeneratorState]] = "executing"; |
| 12 | // restore the saved execution context and resume... |
| 13 | } |
The key implementation detail: at each yield, the engine saves the entire execution context (local variables, instruction pointer, operand stack) into the GeneratorObject on the heap. The function's stack frame is effectively moved from the call stack to the heap. When resumed, it's moved back.
Async generators โ the AsyncGeneratorRequest queue
Async generators add a complication regular generators don't have: callers can queue multiple next() calls before the first resolves. The spec handles this with an internal [[AsyncGeneratorQueue]] โ a list of pending requests, each containing the resolve/reject functions for its result promise.
| 1 | // What happens with multiple queued next() calls: |
| 2 | const gen = asyncGen(); |
| 3 | const p1 = gen.next(); // queued: [req1] |
| 4 | const p2 = gen.next(); // queued: [req1, req2] |
| 5 | const p3 = gen.next(); // queued: [req1, req2, req3] |
| 6 | // Generator processes them FIFO โ one at a time |
| 7 | // Each await inside the generator suspends ONLY that request |
| 8 | // The others wait in the queue |
| 9 | ย |
| 10 | // await inside an async generator suspends differently: |
| 11 | // it suspends the generator AND leaves its execution context |
| 12 | // on the microtask queue, not the heap like a regular yield. |
Symbol.iterator vs Symbol.asyncIterator
| Protocol | Well-known symbol | next() return type | Consumed with |
|---|---|---|---|
| Iterable | [Symbol.iterator] | { value, done } | for...of, spread, destructuring |
| Iterator | [Symbol.iterator] returning self | { value, done } | Same as above |
| Async iterable | [Symbol.asyncIterator] | Promise<{ value, done }> | for await...of |
| Async iterator | [Symbol.asyncIterator] returning self | Promise<{ value, done }> | for await...of |
for...of and for await...of desugared
for...of desugaring
| 1 | // for (const x of iterable) body; |
| 2 | // desugars to: |
| 3 | const iter = iterable[Symbol.iterator](); |
| 4 | let result; |
| 5 | while (!(result = iter.next()).done) { |
| 6 | const x = result.value; |
| 7 | // body |
| 8 | } |
| 9 | // break โ iter.return?.() |
| 10 | // throw โ iter.throw?.(e) |
for await...of desugaring
| 1 | // for await (const x of asyncIterable) body; |
| 2 | // desugars to: |
| 3 | const iter = asyncIterable[Symbol.asyncIterator] |
| 4 | ?? asyncIterable[Symbol.iterator]; |
| 5 | const asyncIter = iter.call(asyncIterable); |
| 6 | let result; |
| 7 | while (!(result = await asyncIter.next()).done) { |
| 8 | const x = result.value; |
| 9 | // body |
| 10 | } |
| 11 | // break โ await asyncIter.return?.() |
Generator-based coroutine pattern (co library)
Before async/await (pre-ES2017), generators were used to write async code that looked synchronous. The co library automated driving the generator โ it called next(resolvedValue) each time a yielded promise resolved, and throw(error) if it rejected. This is exactly what async/await compiles to today:
| 1 | // Before async/await โ generator + co: |
| 2 | co(function* () { |
| 3 | const user = yield fetch("/api/user"); // suspends until resolved |
| 4 | const posts = yield fetch("/api/posts"); // suspends until resolved |
| 5 | return { user, posts }; |
| 6 | }); |
| 7 | ย |
| 8 | // After async/await โ same semantics, nicer syntax: |
| 9 | async function load() { |
| 10 | const user = await fetch("/api/user"); |
| 11 | const posts = await fetch("/api/posts"); |
| 12 | return { user, posts }; |
| 13 | } |
| 14 | // async/await IS generator + promise driver, baked into the language. |
Comparison table
| Kind | Syntax | next() returns | Can await? | Protocol |
|---|---|---|---|---|
| Generator | function* | { value, done } | No | Symbol.iterator |
| Async generator | async function* | Promise<{ value, done }> | Yes | Symbol.asyncIterator |
| Iterable | obj[Symbol.iterator]() | iterator object | No | Symbol.iterator |
| Async iterable | obj[Symbol.asyncIterator]() | async iterator | Yes | Symbol.asyncIterator |
Interview Questions
โ Real questions from real interviews โ with answers.A generator function (`function*`) returns an iterator that can pause and resume via `yield`, preserving local state between calls.
The value passed to `.next(v)` becomes the result of the paused `yield` expression inside the generator. The first `.next()` value is always discarded.
`yield*` delegates iteration to another iterable, transparently forwarding all `.next()`, `.return()`, and `.throw()` calls.
Async generators return `Promise<{ value, done }>` from `.next()` instead of `{ value, done }` synchronously, and support both `yield` and `await`.
Generators are trivially cancellable โ just stop calling `.next()`. A suspended generator holds no resources until resumed.
Memory Game
โ Quick quiz โ lock the concept in long-term memory.Why are generators useful for implementing state machines?