Generators in JavaScript: A Beginner's Guide

Abstract image used to represent Generators in JavaScript: A Beginner's Guide
Image by Dan Meyers.

An ordinary synchronous JavaScript function runs until it returns or throws. Since ECMAScript 6 (ES6), a generator can pause at a yield expression, hand a value back to its caller, and continue from the same point later. That makes generators useful for sequences and controlled iteration, but the extra state is also what makes them easy to misuse.


How Generators Work

Generators are defined using the function* syntax instead of the regular function syntax (the asterisk is what differentiates the two). When a generator is called, it doesn't execute immediately. Instead, it returns an iterator object which can be used to control the generator's execution.

Here's an example of a basic generator which produces a sequence of numbers:

function* generateNumbers() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = generateNumbers();
console.log(generator.next().value);  //=> 1
console.log(generator.next().value);  //=> 2
console.log(generator.next().value);  //=> 3

This is a very basic example, what we're doing is setting up the generateNumbers() generator function. This produces a sequence of numbers using the yield keyword. Each time the next() method is called on the generator object, the generator resumes execution and produces the next value in the sequence.

One of the most common use cases for generator functions is to generate sequences of data that would otherwise be too large to fit into memory. As an example, we could use a generator to generate an infinite sequence of Fibonacci numbers:

function* fibonacci() {
  let a = 0;
  let b = 1;

  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

We can then use the fibonacci() generator function to produce a sequence of Fibonacci numbers. Each time the next() method is called on the fib object, the generator resumes execution and produces the next number in the sequence:

const fib = fibonacci();

console.log(fib.next().value);  //=> 0
console.log(fib.next().value);  //=> 1
console.log(fib.next().value);  //=> 1
console.log(fib.next().value);  //=> 2
console.log(fib.next().value);  //=> 3
console.log(fib.next().value);  //=> 5
console.log(fib.next().value);  //=> 8

You could also for example iterate over it twenty times and then output the sequence to twenty places. Something like:

let sequence = [];
const fib = fibonacci();

[...Array(20)].forEach(() => sequence.push(fib.next().value));

console.log(sequence);
//=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]

More Advanced Generator Techniques

June 2018: ECMAScript 2018 standardised asynchronous iteration. The for await...of example below therefore postdates this article's original August 2017 publication; the ordinary generators above are ES2015.

Generators can be used to implement more advanced functionality, such as asynchronous programming using Promises.

As an example, we can use a generator to implement a function which waits for a specified amount of time before resolving a Promise:

const wait = (ms, value) =>
  new Promise((resolve) => setTimeout(() => resolve(value), ms));

function* generateNumbersWithDelay() {
  yield wait(1000, 1);
  yield wait(1000, 2);
  yield wait(1000, 3);
}

(async () => {
  const generator = generateNumbersWithDelay();
  for await (const value of generator) {
    console.log(value);
  }
})();

In this example, the generateNumbersWithDelay() generator function produces a sequence of numbers with a delay of 1 second between each number. The wait() function returns a Promise that resolves after the specified number of milliseconds.

To consume the sequence of values produced by the generator, we use the for await...of loop. This loop allows us to iterate over the sequence of values, waiting for each Promise to resolve before moving on to the next value.


Potential Issues

Whilst generators are a powerful feature, they can also be tricky to work with, and even trickier to debug when they don't behave as expected.

Generator Instance State

Generator state belongs to the iterator object returned by calling the generator function. Passing that same object between ordinary functions preserves its paused position. A generator is nonreentrant in the narrower sense that it cannot be resumed again whilst it is already executing.

The following example demonstrates that the same generator instance continues its sequence even when one function pauses it and another resumes it:

function* generator() {
  let count = 0;
  while (true) {
    yield count++;
  }
}

function pauseGenerator() {
  const gen = generator();
  console.log(gen.next().value);  //=> 0
  console.log(gen.next().value);  //=> 1
  return gen;
}

function resumeGenerator(gen) {
  console.log(gen.next().value);  //=> 2
  console.log(gen.next().value);  //=> 3
}

const gen = pauseGenerator();
resumeGenerator(gen);

Here, the generator() function produces an infinite sequence of numbers. When pauseGenerator() is called, it creates a new generator object and calls next() twice to produce the first two numbers in the sequence. It then returns the generator object.

The resumeGenerator() function takes a generator object as an argument and calls next() twice more to produce the next two numbers in the sequence.

Running this code prints 0, 1, 2, 3.

When we pass this generator object to resumeGenerator(), it resumes execution where pauseGenerator() left off. The next call reads count as 2, then advances it; the following call therefore yields 3 when resumeGenerator requests the next value. A separate call to generator() would instead create a separate sequence starting from 0.

All this to say: execution context does not reset a generator; generatorobject identity determines which paused state resumes.

Other Potential Issues

Two other things to consider when using generators:

  • Generators can be difficult to debug because they can be paused and resumed at any time during execution. This can make it difficult to trace the flow of execution through a generator function.
  • A paused generator can keep local values and resources alive for as long as its state is still reachable. An infinite sequence isn't itself a memory leak: the values are produced on demand. Avoid retaining generator objects you no longer need, and clean up any resources they hold.

Wrapping Up

A generator earns the extra state when its caller needs to pull values one at a time or resume a sequence from a known point. Ordinary collections and loops are usually clearer when no pauseandresume behaviour is required. The function* syntax is straightforward; the design choice is whether resumable execution belongs in the problem at all.


Have a complex web platform issue?

Tell me what is blocked, what has changed, and what needs to be true after the fix. I'll come back with a practical next step.