DEV Community

Cover image for An Introduction to JavaScript Generators
Alice Kallaugher
Alice Kallaugher

Posted on • Originally published at giantmachines.com

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):

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

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):

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
    • c. 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:

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().

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

Diagram showing the input and output of generators. All content is included in the text.

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 was originally posted on the Giant Machines blog.

Top comments (2)

Collapse
 
katnel20 profile image
Katie Nelson

I really enjoyed this great post Alice.
I'm trying to think of a good use-case for a generator.

One question I have is:
Is there a way to restart the process once you iterate to the end of a generator function?

Collapse
 
kallaugher profile image
Alice Kallaugher

Thanks Katie!

To answer your question: you can't restart a generator once it's done. You would have to call the generator function again.