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:
The generator is created
-
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 followingyield
- c.
done
is false because we haven’t reached a return statement or the end of the generator function
- The generator function evaluates up to the first
-
next()
is called for a second time:- The generator function evaluation resumes
- The
return
statement is reached -
value
is the result of thereturn
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()
:
-
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 firstyield
- It reaches the first
-
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
- The argument provides the value of the constant assigned to the first yield statement (therefore
-
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
- There is no argument to provide the value of the constant assigned to the second yield statement (therefore
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
Here’s a quick summary of the main takeaways from this post.
Generators:
- pause with
yield
and restart withnext()
- 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 thenext()
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)
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?
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.