Command Palette

Search for a command to run...

Hectal
PHASE 14Advanced ES2015+ ~27 min· topic 2 of 9

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().

steps.jswhole filejavascript
function* steps() {
  console.log("step 1");
  yield 1;
  console.log("step 2");
  yield 2;
}
const g = steps(); // prints nothing

02Each 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 }.

Each next() runs to the next yielddiagram
Rendering diagram…

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.

range.jswhole filejavascript
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.

Build a lazy pipelinediagram
Rendering diagram…

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. 1

    Forget to stop

    In the lazy example, predict what happens if you write [...evens] without take. (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. 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. 3

    Throw into a generator

    Add try { const name = yield "What is your name?"; } catch (e) { console.log("caught", e.message); } style handling, then call chat.throw(new Error("no name")) after the first next(). Predict where the error lands.

Code & diagrams

Pausing and resuming ES2015+ New tab

The returned `"end"` shows up in `next()` but not in the spread: consumers only collect yielded values.

Preview

Press Run to see the page.

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' ]
Lazy and infinite sequences ES2015+ New tab

`naturals()` never ends, yet the program finishes: `take` returns after 5 items, which also closes the generators behind it.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

[ 2, 4, 6, 8, 10 ]
[ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34 ]
yield* for trees and iterable classes ES2015+ New tab

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

root
- a
- - a1
- b
[ 'Intro', 'Rain', 'Outro' ]
Two-way next(), return() and finally ES2015+ New tab

Preview

Press Run to see the page.

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; }); }

terminal
$ Console
── what you'll see ──
Uncaught SyntaxError: Unexpected identifier 'x'

Break #2

Expect the body to run on call

Write function* log() { console.log("hi"); } and call log();

terminal
$ Console
── what you'll see ──
(nothing is printed)

Break #3

Loop over the same generator twice

Write const nums = take(naturals(), 3); console.log([...nums]); console.log([...nums]);

terminal
$ Console
── what you'll see ──
[ 1, 2, 3 ]
[]

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 try blocks) 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/await is specified on the same machinery: an async function is like a generator whose yields are awaits, driven by a promise-based runner. Transpilers turned async code into generators (and generators into state machines with regenerator) 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. 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's next() runs the body until the next yield, then pauses (stops in the middle, keeping all its local variables) and returns { value: <what was yielded>, done: false }.

  2. 2

    When the body finishes, or reaches a return, next() returns { value: <the returned value>, done: true }. After that every next() gives { value: undefined, done: true }. The generator object follows the iteration protocol from Topic 14.1: it is an iterator (it has next()) and an iterable (its [Symbol.iterator]() returns itself). So for...of, spread and destructuring work on it, but each generator object can be looped once. Note that for...of and spread ignore the final return value: only yielded values are items.

  3. 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 per next(). You combine it with something that stops, like a take(n) helper or a break. Laziness also saves memory: a pipeline of generators processes one item at a time instead of building big arrays at each step.

  4. 4

    yield is two-way. The value you pass to next(x) becomes the **result of the paused yield expression**, so const answer = yield "question" receives what the caller sends next. (The very first next() only starts the body, so any argument to it is ignored.) gen.return(v) finishes the generator early, running any finally blocks, and gen.throw(err) throws an error **at the paused yield**, which the generator can catch with try...catch.

  5. 5

    yield* other delegates 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. 6

    Arrow functions can't be generators, and yield only works directly inside the generator's own body, not inside a callback such as forEach(x => ...). Async generators (async function*, ES2018) combine both ideas: they can await and yield, and you consume them with for await...of, which suits paging through an API one page at a time (Phase 10 covers promises and await).

Explain it without notes

01

What happens, step by step, when you call a generator function and then call next() three times?

02

Why are generators described as lazy, and why does that make infinite sequences possible?

03

How do next(value), return() and throw() let the caller control a generator?

04

What does yield* do, and when would you use it?

Practice

01

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 ] ].

02

Write an infinite generator idMaker(prefix) that yields "user-1", "user-2", ... and log the first three ids using destructuring.

03

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/filter chain 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* and yield and 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() and finally.

  • I can use yield* to delegate, including recursively.