Iterators and generators let you define custom iteration behavior — for lazy sequences, infinite streams, and pausable functions.
Think of it like this:
- An iterator is like a bookmark in a book — it remembers where you are and hands you the next page each time you ask. Each call to
.next()turns one page. - A generator is like a recipe — you can pause after each step (
yield), do something else, and resume right where you left off (next()). The ingredients and current step are preserved between pauses.
This article covers Symbol.iterator, generator functions, yield*, and patterns like range and take.
Prerequisites: Async #5 — async/await Under the Hood
1. The Iterator Protocol
Before the code, a quick distinction:
Iterable Iterator
"I can be looped over" "I track WHERE you are in the loop"
Has [Symbol.iterator]() Has next() → { value, done }
↓ ↓
Returns an iterator Each call moves forward
An object is an iterator if it has a next() method that returns { value, done }. An object is iterable if it has a [Symbol.iterator]() method that returns an iterator. for...of only works with iterables — it calls [Symbol.iterator]() under the hood to get an iterator, then calls .next() until done is true.
Loading editor...
2. Custom Iterable — Make an Object Work with for...of
Loading editor...
3. Generator Functions — function* and yield
A generator function returns a generator object that is BOTH iterable and an iterator. Each yield pauses execution — the function doesn't run to completion. Next .next() resumes from where it left off.
Loading editor...
4. yield* — Delegating to Another Iterable
yield* delegates to another iterable — it yields every value from that iterable one by one:
Loading editor...
5. Infinite Generators — Lazy Sequences
Generators are lazy — they only compute values when asked. This enables infinite sequences:
Loading editor...
6. Two-Way Communication — Passing Values INTO a Generator
This is the most mind-bending generator feature and the foundation of async/await. Data flows BOTH ways:
- Outward:
yield expressionsendsexpressionout via{ value: expression, done: false } - Inward:
.next(value)sendsvalueback IN, and it becomes the result of theyieldexpression
gen.next(10) ──→ [PAUSED AT yield] → value goes IN
const a = yield "What is a?";
──→ "What is a?" goes OUT → caller gets { value: "What is a?", done: false }
.next(value) passes value back to where yield paused:
Loading editor...
This is what powers the async/await desugaring — the runner passes resolved promise values back via .next().
7. gen.throw() and gen.return()
Loading editor...
8. Practical Use — State Machines
Generators make excellent state machines because yield naturally represents a transition:
Loading editor...
No state variable, no switch/case — the generator body IS the state machine. Each yield is a state; each .next() is a transition.
Key Takeaways
- Iterable = has
[Symbol.iterator]()— works withfor...of, spread, destructuring. - Iterator = has
next()returning{ value, done }. - Generator =
function*that is BOTH iterable AND iterator —yieldpauses,next()resumes. yield*delegates to another iterable — cleaner than a for-loop over nested items.- Two-way:
.next(value)passes data INTO the generator;.throw()and.return()control flow. - Generators are lazy — values are computed on demand, enabling infinite sequences.
Next: Async #7 — Async Iteration — for await...of, async generators, and async iterables.
