๐Ÿณ IntuitiveFE
Login
โ† All concepts

Generators & Async Iterators

โฑ๏ธ ~4-minute bite ยท solve the sandbox to master

0%lesson
๐Ÿง’

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.

js
1function* 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}
9const reader = book();
10reader.next(); // logs "Chapter 1" โ†’ { value: "end of chapter 1", done: false }
11reader.next(); // logs "Chapter 2" โ†’ { value: "end of chapter 2", done: false }
12reader.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)

js
1// Must hold all 1M numbers at once
2const nums = Array.from(
3 { length: 1_000_000 },
4 (_, i) => i
5);
6// ~4 MB in memory immediately

Lazy โ€” generator (O(1) memory)

js
1function* 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:

js
1function* range(n) {
2 for (let i = 0; i < n; i++) yield i;
3}
4ย 
5// All of these work out of the box:
6for (const x of range(3)) { } // for...of
7const arr = [...range(3)]; // spread โ†’ [0, 1, 2]
8const [a, b] = range(3); // destructuring โ†’ 0, 1
9const set = new Set(range(5)); // constructors that accept iterables
๐ŸŽ›๏ธ

Interactive Sandbox

โ€” Move something, see it react instantly.

Pattern

Code

const gen = counter(0);
1function* counter(start = 0) {
2 let i = start;
3 while (true) {
4 yield i++;
5 }
6}

Generator state

not started

Last returned

โ€”

Local variables

Call next() to start

Call trace

Press next โ†’ to call gen.next()
Gotcha: done is never true for an infinite generator. Breaking out of a for...of loop calls gen.return() which sets done: true.
Insight: Infinite sequences are possible because generators only compute the next value on demand โ€” the entire sequence never exists in memory.
Explored:โ™พ๏ธ๐Ÿ“โ†”๏ธ๐Ÿ”—โšก
๐ŸŽฏ

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.

Try it
๐ŸŽฏ

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.

js
1// Generator: take first 3 from infinite stream โ€” O(1) memory
2function* naturals() { let n = 0; while (true) yield n++; }
3const 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.

js
1function* echo() {
2 while (true) {
3 const received = yield "ready";
4 console.log("Got:", received);
5 }
6}
7const g = echo();
8g.next(); // โ† first call: starts the generator, runs until yield, returns { value: "ready" }
9g.next("hello"); // โ† "Got: hello", returns { value: "ready" }
10g.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.

js
1async 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ย 
10for 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.

js
1// Redux-Saga uses generators exactly like this:
2function* 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:

js
1// Simplified spec pseudocode for GeneratorResume:
2function 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.

js
1// What happens with multiple queued next() calls:
2const gen = asyncGen();
3const p1 = gen.next(); // queued: [req1]
4const p2 = gen.next(); // queued: [req1, req2]
5const 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

ProtocolWell-known symbolnext() return typeConsumed 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 selfPromise<{ value, done }>for await...of

for...of and for await...of desugared

for...of desugaring

js
1// for (const x of iterable) body;
2// desugars to:
3const iter = iterable[Symbol.iterator]();
4let result;
5while (!(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

js
1// for await (const x of asyncIterable) body;
2// desugars to:
3const iter = asyncIterable[Symbol.asyncIterator]
4 ?? asyncIterable[Symbol.iterator];
5const asyncIter = iter.call(asyncIterable);
6let result;
7while (!(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:

js
1// Before async/await โ€” generator + co:
2co(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:
9async 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

KindSyntaxnext() returnsCan await?Protocol
Generatorfunction*{ value, done }NoSymbol.iterator
Async generatorasync function*Promise<{ value, done }>YesSymbol.asyncIterator
Iterableobj[Symbol.iterator]()iterator objectNoSymbol.iterator
Async iterableobj[Symbol.asyncIterator]()async iteratorYesSymbol.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.
1/4

Why are generators useful for implementing state machines?