Generators in JavaScript: A Beginner's Guide

Hero image for Generators in JavaScript: A Beginner's Guide. Image by Dan Meyers.
Hero image for 'Generators in JavaScript: A Beginner's Guide.' Image by Dan Meyers.

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 realworld 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);  //=> 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);  //=> 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);  //=> 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

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) => new Promise((resolve) => setTimeout(resolve, ms));function* generateNumbersWithDelay() {  yield wait(1000);  yield 1;  yield wait(1000);  yield 2;  yield wait(1000);  yield 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

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

The Context Problem

The biggest gotcha (and potential for unexpected behaviour) is that generators are not reentrant; they cannot be resumed from a paused state by a different execution context. So, if a generator is paused in one function and then resumed in another function, it may not behave as expected.

This is an important thing for developers to understand, so please forgive me as I present another example that hopefully demonstrates how generators can behave unexpectedly when paused in one context and resumed in another. Take the following code:

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.

However, the actual output is: 0, 1, 1, 2.

This unexpected behaviour is down to the fact that the generator is paused and resumed in two different execution contexts. When we call 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, producing the third number in the sequence. However, since the generator was paused in a different execution context, the value of count is no longer 2, but 1.

All this to say: generator functions called in different contexts, will not behave sequentially as you might expect.

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.


Categories:

  1. ES6
  2. Front‑End Development
  3. JavaScript