Topic 14.2
Generators
In one line
A generator function (function*) can pause at each yield and resume later; calling it returns a generator object that is both an iterator and an iterable, which makes lazy and even endless sequences a few lines of code.
Think of it like this
Reading a bedtime story in parts. Each night you read until a bookmark, close the book, and the next night you open it at the bookmark and carry on. The story remembers exactly where it was. A generator is a function with bookmarks: each yield is a bookmark where it stops and hands you something, and next() opens the book again at that spot.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Generator function
- A function written with
function*that can pause and resume. Calling it returns a generator object. - Generator object
- What a generator function returns: an iterator you drive with
next(). - yield
- The keyword that pauses a generator and hands out a value.
- Pause / resume
- Stop in the middle of a function, keeping its variables, and later carry on from the same spot.
- Lazy sequence
- A list of values that are worked out one at a time, only when someone asks.
- Infinite sequence
- A sequence with no end, like all counting numbers. Safe with generators because only the requested values are made.
- yield*
- Hands out every value of another iterable, one by one, from inside a generator.
- Delegation
- Letting another generator or iterable produce some of your values.
Step by step
01Calling it runs nothing
const g = steps() only creates the generator object. No line of the body has run yet. The body starts on the first next().
function* steps() {
console.log("step 1");
yield 1;
console.log("step 2");
yield 2;
}
const g = steps(); // prints nothing02Each next() runs to the next yield
The first next() prints step 1 and stops at yield 1. The second resumes right after it, prints step 2, and stops at yield 2. The third runs off the end of the body: { value: undefined, done: true }.
03Replace a hand-written iterator
The Range class from Topic 14.1 needed a next() method and a closure. As a generator method it is three lines, and it follows the protocol exactly, including return() on early exit.
class Range {
constructor(from, to) {
this.from = from;
this.to = to;
}
*[Symbol.iterator]() {
for (let n = this.from; n <= this.to; n++) yield n;
}
}
console.log([...new Range(1, 4)]); // [ 1, 2, 3, 4 ]04Build a lazy pipeline
Small generators chain together. Each pulls one item from the one before it, only when the next stage asks. Nothing makes an array until the final spread.
05Talk back with next(value)
next("Asha") resumes the generator, and the paused yield expression evaluates to "Asha". This two-way channel is how libraries such as redux-saga, and the old co library that inspired async/await, drove code step by step.
06Clean up with finally
Put cleanup in try...finally around the yields. Whether the loop finishes, breaks, or someone calls return(), the finally block runs.
Try it yourself
- 1
Forget to stop
In the lazy example, predict what happens if you write
[...evens]withouttake. (Don't run it in your own browser tab; the preview stops runaway loops, but a generator spread has no loop body to guard, so the tab may freeze. Reason it out instead.) - 2
Send a value first
In the conversation example, change the first call to
chat.next("ignored"). Predict whether anything changes, then run it. - 3
Throw into a generator
Add
try { const name = yield "What is your name?"; } catch (e) { console.log("caught", e.message); }style handling, then callchat.throw(new Error("no name"))after the firstnext(). Predict where the error lands.
Code & diagrams
The returned `"end"` shows up in `next()` but not in the spread: consumers only collect yielded values.
Preview
Console
Nothing logged yet.Expected console output
created, nothing ran yet
started
{ value: 'hello', done: false }
resumed
{ value: 'world', done: false }
{ value: 'end', done: true }
{ value: undefined, done: true }
started
resumed
[ 'hello', 'world' ]`naturals()` never ends, yet the program finishes: `take` returns after 5 items, which also closes the generators behind it.
Preview
Console
Nothing logged yet.Expected console output
[ 2, 4, 6, 8, 10 ]
[ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ]Preview
Console
Nothing logged yet.Expected console output
root
- a
- - a1
- b
[ 'Intro', 'Rain', 'Outro' ]Preview
Console
Nothing logged yet.Expected console output
What is your name?
Hi Asha! Favourite food?
{ value: 'Asha likes dosa', done: true }
1
2
finally: cleaning up
finally: cleaning up
{ value: 'stopped', done: true }
{ value: undefined, done: true }Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
yield inside a callback
Write function* each(list) { list.forEach((x) => { yield x; }); }
Break #2
Expect the body to run on call
Write function* log() { console.log("hi"); } and call log();
Break #3
Loop over the same generator twice
Write const nums = take(naturals(), 3); console.log([...nums]); console.log([...nums]);
Myth vs fact
Myth
Generators run in parallel or on another thread.
Fact
They run on the same thread, one step at a time, only when you call next(). Pausing is cooperative, not concurrent.
Myth
An infinite generator loops forever and freezes the page.
Fact
It only runs until the next yield. It's the consumer that must stop: take, break, or destructuring a fixed number of items.
Myth
async/await made generators obsolete.
Fact
async/await replaced generators for async control flow, but generators remain the clean way to write iterators, lazy pipelines and step-by-step algorithms.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A paused generator keeps its whole stack frame (locals, the position in the code, any
tryblocks) alive on the heap. That's cheap for one, but a thousand paused generators holding big arrays keep all that memory reachable until they finish or are dropped. - ▸
async/awaitis specified on the same machinery: an async function is like a generator whoseyields areawaits, driven by a promise-based runner. Transpilers turnedasynccode into generators (and generators into state machines withregenerator) for old browsers. - ▸
Generator objects inherit from the generator function's
prototype, which inherits from%GeneratorPrototype%. Because they're built-in iterators, ES2025 iterator helpers work on them directly:naturals().filter((n) => n % 2 === 0).take(5).toArray(). - ▸
Laziness changes when errors happen: an invalid argument checked inside the generator body only throws on the first
next(), not at the call. If you want eager validation, check in a normal wrapper function that then returns the generator.
Remember this
- 1
A generator function is written with a star:
function* name() { ... }(or*name() { ... }as a method). Calling it does not run its body. It returns a generator object straight away. Each call to that object'snext()runs the body until the nextyield, then pauses (stops in the middle, keeping all its local variables) and returns{ value: <what was yielded>, done: false }. - 2
When the body finishes, or reaches a
return,next()returns{ value: <the returned value>, done: true }. After that everynext()gives{ value: undefined, done: true }. The generator object follows the iteration protocol from Topic 14.1: it is an iterator (it hasnext()) and an iterable (its[Symbol.iterator]()returns itself). Sofor...of, spread and destructuring work on it, but each generator object can be looped once. Note thatfor...ofand spread ignore the finalreturnvalue: onlyyielded values are items. - 3
Generators are lazy: they work out a value only when asked. That makes infinite sequences safe:
function* naturals() { let n = 1; while (true) yield n++; }never finishes on its own, but it only runs one step pernext(). You combine it with something that stops, like atake(n)helper or abreak. Laziness also saves memory: a pipeline of generators processes one item at a time instead of building big arrays at each step. - 4
yieldis two-way. The value you pass tonext(x)becomes the **result of the pausedyieldexpression**, soconst answer = yield "question"receives what the caller sends next. (The very firstnext()only starts the body, so any argument to it is ignored.)gen.return(v)finishes the generator early, running anyfinallyblocks, andgen.throw(err)throws an error **at the pausedyield**, which the generator can catch withtry...catch. - 5
yield* otherdelegates to another iterable: it yields each of its values in turn, then continues. It is how you write recursive generators (walking a tree), and how you forward one generator's items from another. The easiest way to make a class iterable is a generator method:*[Symbol.iterator]() { yield* this.items; }. - 6
Arrow functions can't be generators, and
yieldonly works directly inside the generator's own body, not inside a callback such asforEach(x => ...). Async generators (async function*, ES2018) combine both ideas: they canawaitandyield, and you consume them withfor await...of, which suits paging through an API one page at a time (Phase 10 covers promises andawait).
Explain it without notes
What happens, step by step, when you call a generator function and then call next() three times?
Why are generators described as lazy, and why does that make infinite sequences possible?
How do next(value), return() and throw() let the caller control a generator?
What does yield* do, and when would you use it?
Practice
Write a generator chunks(array, size) that yields slices of size items. [...chunks([1, 2, 3, 4, 5], 2)] should give [ [ 1, 2 ], [ 3, 4 ], [ 5 ] ].
Write an infinite generator idMaker(prefix) that yields "user-1", "user-2", ... and log the first three ids using destructuring.
Write a generator cycle(items) that repeats the items forever, and use a for...of with a counter to log the first 5 values of cycle(["red", "green"]).
Trade-offs
- ↔
Generators make lazy code short and memory-light, but each step is a function resume, so for small arrays a plain
map/filterchain is usually faster and easier to debug. - ↔
A generator object can only be consumed once; returning an array is reusable and indexable, at the cost of building everything up front.
Done when you can
I can write a generator with
function*andyieldand predict when its body runs.I can build a lazy pipeline and use an infinite generator safely.
I can make a class iterable with
*[Symbol.iterator]().I can use
next(value),return(),throw()andfinally.I can use
yield*to delegate, including recursively.