Course outline · 0% complete

0/27 lessons0%

Course overview →

Generators: pausable functions

lesson 7-2 · ~11 min · 18/27

function* writes iterators for you

That hand-written next() in lesson 7-1 was clunky. A generator function, written function*, builds the iterator for you.

Three rules describe the whole mechanism.

  • Calling it runs none of the body. It returns a generator object, which is an iterator.
  • Each next() runs the body until the next yield, which pauses the function and hands out that value.
  • The function's local variables survive between pauses, like a closure that sleeps.

That third point is what makes generators feel unusual. A normal function's locals die when it returns, and a generator's locals persist across an arbitrary number of resumptions.

Generators earn their keep whenever a sequence is expensive, endless, or paged, including ID factories, paginated API readers, and streaming parsers.

They are iterable as well as iterators, which is a small but important detail. A generator object implements Symbol.iterator returning itself, so for...of consumes it directly.

The spread syntax works too, since ... expands any iterable into individual values, and unit 8 covers it properly. Be careful spreading an endless generator, because that one really does hang.

Where a generator pauses

Nothing prints at creation.

function* steps() {
  console.log("running until first yield");
  yield 1;
  console.log("resumed, running until second yield");
  yield 2;
}

const g = steps();
console.log("created, nothing ran yet");
console.log(g.next().value);
console.log(g.next().value);

Output

created, nothing ran yet
running until first yield
1
resumed, running until second yield
2

Each next executes only up to the following yield, then freezes mid-function.

The first line of output proves the call ran zero lines of the body. steps() built a paused generator and returned.

The interleaving is the thing to read carefully. running until first yield prints during the first next() call, before that call returns its value.

A third next() would run to the end of the function and return { value: undefined, done: true }, with no further output.

This is the same suspend-and-resume machinery that async/await uses, which is why the pausing felt familiar in unit 6. Generators pause on yield and async functions pause on await.

function* count()yield 1yield 2yield 3done{ value: 1, done: false }next() resumes{ value: 2, done: false }{ value: 3, done: false }{ done: true }The function body keeps its place between calls, and nothing runs until next() asks.
A generator freezes at each yield and picks up exactly where it stopped on the next call to next().

Lazy sequences

Because a generator only computes when asked, it can describe an infinite sequence safely.

function* naturals() {
  let n = 1;
  while (true) yield n++;
}

No infinite loop actually runs. Values are produced one next() at a time, and the consumer decides when to stop.

The word for this is laziness, meaning work happens on demand rather than up front. An array of the first million naturals costs a million slots of memory, and this generator costs one variable.

This style shows up in real code as ID generators, paginated data readers, and streaming parsers. Each one is a sequence whose length is unknown or unbounded at the point where it is defined.

Consuming it safely means always bounding the consumer. A for...of with a break, or a take(n) helper, keeps the loop finite.

The dangerous operations are the ones that consume everything. [...naturals()], Array.from(naturals()), and Promise.all over it all hang, because they ask for every value.

An endless generator that never spins

The while (true) runs exactly as much as it is asked to.

function* naturals() {
  let n = 1;
  while (true) yield n++;
}

const it = naturals();
console.log(it.next().value);
console.log(it.next().value);
console.log(it.next().value);

Output

1
2
3

Each next executes exactly one loop iteration and freezes at the yield, so the consumer decides how much work ever happens.

n lives across all three calls, which is the persistent-locals rule doing the counting. There is no external state anywhere.

Compare this to makeCounter from lesson 1-2, which did the same job with a closure. The generator version reads as a loop and plugs into for...of for free.

for (const n of naturals()) { if (n > 3) break; console.log(n); } is the safe way to loop it, and the break is what keeps the program finite.

Calling naturals() twice gives two independent counters, each with its own n, exactly as two calls to a maker function gave two counts.

It runs nothing and returns a paused generator object.

The call itself executes zero lines of the body, so even a console.log on the first line stays silent.

Only next() advances it, one yield at a time.

The example above proved it, since created, nothing ran yet printed before any body output.

That deferral is the practical difference from a normal function, and it has a useful consequence. Building a generator is free, so you can create one and decide later whether to consume it.

Errors follow the same rule. An argument validation throw on the first line of a generator does not fire at call time, it fires on the first next(), which is a real gotcha when a generator validates its inputs.

evens

A bounded generator driven by an ordinary loop.

function* evens(limit) {
  for (let n = 2; n <= limit; n += 2) {
    yield n;
  }
}

for (const n of evens(6)) {
  console.log(n);
}

Output

2
4
6

A normal for loop inside the generator body works, starting at 2 and stepping by 2.

yield n pauses once per number, and no return is needed. Falling off the end of the body finishes the iterator.

The loop runs to completion here, so the generator is exhausted afterward. Iterating evens(6) again means calling it again, since a generator object cannot be restarted.

The for...of is what drives it, calling next() on each pass and stopping when done becomes true. Nothing in the generator knows how it is being consumed.

evens(1) yields nothing at all, which is correct rather than an error. The loop condition fails immediately and the iterator reports done on the first next().

Delegating is the natural extension worth knowing. yield* inside a generator forwards every value from another iterable, so function* both() { yield* evens(6); yield* evens(4); } composes them without a manual loop.