Giant Machines LogoGiant Machines Logo
Giant Insights

An Introduction to JavaScript Generators

One of the fundamentals of JavaScript is that it is single-threaded, meaning that two pieces of code cannot run at the same time. If we call a function, we expect it to run to completion, blocking any other code from running. This presents challenges for any task where you need to wait for something to happen (for example, waiting for an API response). We have different tools at our disposal to help with this, including callback functions, promises, and more recently `async/await`, introduced with ES8.

A lesser known, but still very powerful tool was introduced earlier, with ES6: generators. These are similar to `async/await` in that they let us write asynchronous code in a linear, straightforward fashion. However, they also provide the ability to pause and restart a function, without blocking the execution of other code -- exactly what we’re used to not being able to do in JavaScript!

I first encountered generators through redux-saga, an excellent library for handling side effects in Redux. I was curious to learn about how they worked, and found them a little unintuitive at first. I spent some time digging into them, and in this post I’ll share what I found.

You may recognize them from their somewhat unique syntax, with a star after the function declaration and the use of the `yield` keyword (which can only be used within a generator function):

-- CODE language-js --
function* generatorFunc() {
  yield;
}

As their name suggests, generators generate a sequence of values. Each time a generator is paused, it returns a new value, and each time it’s restarted it can take in a new argument. Following how the input and output are used can be a little tricky, so I’m going to focus on these two aspects, breaking down how generators both generate and consume data. 

Generating data

Generators are a type of iterator, which are objects that define a sequence (one example is the array iterator). Iterators must have a `next()` method, which is used to traverse the sequence. Each time `next()` is called it returns an iterator response, which specifies whether the sequence is done as well as the next value in the sequence (or the return value if the sequence is done).

-- CODE language-js --
const iterator = {
    next = () => ({
value: any,
done: boolean
    })
}

Learn more about the iterator protocol.

Generators have additional behavior: they are a specific kind of iterator, returned by a generator function. When the iterator’s `next()` method is called, the generator function will execute until it reaches one of the following:

  • `yield` keyword (pauses the execution)
  • `return` statement (ends the execution)
  • end of the generator function (ends the execution)
  • `throw` keyword (throws an exception)

Here’s an example (with `throw` omitted for simplicity):

-- CODE language-js --
function* generatorFunc() {
  yield 1 + 1;
  return 2 + 2;
}

// 1.
const generatorObj = generatorFunc();

// 2.
generatorObj.next();
// returns { value: 2, done: false };

// 3.
generatorObj.next();
// returns { value: 4, done: true };

View code in a jsfiddle

Let’s break down what’s happening:

1. The generator is created

2. `next()` is called for the first time:

  • The generator function evaluates up to the first `yield`, and then pauses
  • `value` is the result of the expression following `yield`
  • `done` is false because we haven’t reached a `return` statement or the end of the generator function 

3. `next()` is called for a second time:

  • The generator function evaluation resumes
  • The `return` statement is reached
  • `value` is the result of the `return` statement
  • `done` is `true`, and the generator object has been consumed

The sequence of values can also be retrieved without calling `next()` explicitly, using array destructuring, the spread operator, or a simple `for` loop:

-- CODE language-js --
function* generatorFunc() {
  yield 1 + 1;
  yield 1 + 2;   
  return 2 + 2;
}

const [a, b, c] = generatorFunc();
// a = 2, b = 3, c = undefined

const values = [...generatorFunc()];
// values = [2, 3];

const vals = [];
for (const val of generatorFunc()) {
vals.push(val);
}
// vals = [2, 3]

View code in a jsfiddle

One important note here is that these three ways of retrieving values from a generator only take into account the yield expressions, ignoring the value from the return statement. 

Consuming data

So far we’ve looked at how generators passively generate a sequence of values; now, let’s focus on how they take in data. Most standard iterators cannot accept arguments (e.g. array iterators or set iterators), but generators can, by passing an argument to `next()`.

-- CODE language-js --
function* generatorFunc() {
  const a = yield 1 + 1;
  const b = yield 1 + 2; 
  return 2 + 2;
}

const generatorObj = generatorFunc();

// 1.
generatorObj.next(‘value 1’);
// returns { value: 2, done: false }

// 2.
generatorObj.next('value 2');
// returns { value: 3, done: false }
// a = 'value 2';

// 3.
generatorObj.next();
// returns { value: 4, done: true}
// b = undefined

View code in a jsfiddle

Let's break down the order of execution in a more granular way. We'll start by focusing on the value of the variables assigned to the `yield` expression, and the value from the iterator response returned from `next()`.

1. `next()` is called for the first time, with an argument of `'value 1'`

  • It reaches the first `yield` and pauses
  • The value returned by `next()` is the result of the expression following the first `yield`

2. `next()` is called for the second time, with an argument of `'value 2'`

  • The argument provides the value of the constant assigned to the first `yield` statement (therefore `a = 'value 2'`)
  • It reaches the second `yield` and pauses
  • The value returned by `next()` is the result of the expression following the second `yield`

3. `next()` is called for the second time, with no argument

  • There is no argument to provide the value of the constant assigned to the second yield statement (therefore `b = undefined`)
  • It reaches the `return` statement and ends
  • The value returned by `next()` is the result of the `return` statement

The most important thing to grasp here is that the argument to `next()` provides the value for the `yield` that had previously paused execution of the generator function. The argument passed to the first `next()` call is ignored.

Summary

The input and output of a generator.

Here’s a quick summary of the main takeaways from this post.

Generators:

  • pause with `yield` and restart with `next()`
  • return a new value each time the function pauses or ends
  • set each return value based on the expression following the `yield` that paused the function
  • take in data through arguments passed to `next()`
  • set the value of the variable assigned to a `yield` statement based on the arguments passed to the `next()` call that restarted the function

I hope you’ve enjoyed this quick dive into generators! If you want to dig in deeper, I recommend reading the Generators chapter of ‘Exploring ES6’ by Axel Rauschmayer, which was very helpful in writing this article. If you want to see generators in use, redux-saga is definitely worth checking out as well.

Let me know in the comments how you’ve used generators, or if you have any questions!

This post also appears on Medium, where we invite you to share your thoughts and feedback!

Images

About the Author

Alice is a Senior Software Engineer at Giant Machines

Other stories