Generators in JavaScript: A Beginner's Guide

Generators are a powerful feature introduced in ECMAScript 6 (ES6) that allows functions to be paused and resumed at any time. This means that instead of returning a value and exiting as a regular function would, generator functions can produce a sequence of values that can be iterated over, one value at a time.
In this article, I'll help you explore how generators work and how they can be used in real‑world development.
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); //=> 1console.log(generator.next().value); //=> 2console.log(generator.next().value); //=> 3This 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); //=> 0console.log(fib.next().value); //=> 1console.log(fib.next().value); //=> 1console.log(fib.next().value); //=> 2console.log(fib.next().value); //=> 3console.log(fib.next().value); //=> 5console.log(fib.next().value); //=> 8You 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 post‑dates 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 non‑re‑entrant 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. The pauseGenerator() function 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.
When we run this code, we would expect the output to be: 0, 1, 2, 3.
The actual output is: 0, 1, 2, 3.
This behaviour follows from passing one generator object between the two functions. When pauseGenerator(), it creates a new generator object and calls next() twice, producing the first two numbers in the sequence. It then returns the generator object.
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; generator‑object 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.
- Generators can cause memory leaks if they are not properly managed. Since generators can produce an infinite sequence of values, it's important to ensure that they are properly terminated when they are no longer needed.
Wrapping Up
Hopefully, this has helped shed some light on what generators are, and how they can be used. Generators are a powerful feature in JavaScript that allow functions to produce sequences of values that can be iterated over one at a time. They can be used to implement advanced functionality such as asynchronous programming using Promises.
However, they can also be tricky to work with and may cause issues if not properly managed. By understanding how generators work and using them when a situation requires them, developers can take advantage of this powerful feature to create more efficient and flexible code in their JavaScript applications.