JavaScript Generators Explained, But On A Senior-Level
Generators are powerful and underused in JavaScript.
And many tutorials on generators only scratch the surface. In this article, you're going to go deep and you'll develop an advanced understanding about theory behind generators.
Generators are most commonly seen in sagas, but there are more use cases, and you're going to see some of them in this article.
The short answer to the question "What is a generator?" is:
Generators are pull streams in JavaScript.
Let's dissect this definition and then jump into some examples.
You need to understand two terms: "pull" and "stream".
What is a stream?
A stream is data over time. There are two types of streams: push streams and pull streams.
What is a push stream?
A push stream is a mechanism where you are NOT in control WHEN the data comes through.
Examples for push streams include:
- a websocket,
- reading a file from disk, and
- server-sent events.
You can see a JavaScript example of a pull stream using Node.js to read a large file from disk below.
What is a pull stream?
A pull stream is when you ARE in control WHEN you want to request the data.
You will see code examples for pull streams in JavaScript soon when you're going to see generator code, but first you need to understand another concept.
Lazy vs. eager
In programming, data can be processed in two fundamental ways: eagerly or lazily.
Eager
Eager means data is evaluated immediately, regardless of whether the result is needed in that moment. A push stream is eager. (Other examples: array methods, promises)
You might be thinking: "Okay, but why are promises eager? Their result comes in late."
Promises in JavaScript exhibit eager evaluation for several reasons.
- Immediate Execution: The function passed to a new Promise (known as the executor function) is executed immediately when the Promise is constructed.
- Irreversible Operations: Once the executor function begins executing, it cannot be stopped or paused by the consuming code. The results of the operation it performs (either resolution or rejection) will be queued in the JavaScript event loop to be handled as soon as possible.
- No Lazy Option: A promise lacks any built-in mechanism to defer or cancel the execution of its executor until a value is needed.
- side effects: The eager nature of promises means that any side effects included in the executor (like API calls, timeouts, or I/O operations) will happen immediately as part of the promise creation.
The following example demonstrates how demonstrates how promises are executed immediately.
This results in the following output.
Lazy
Lazy means only evaluated when the value is needed (not before). A pull stream is lazy.
A synchronous example would be the operand selector operators.
When you run this code, you'll observe the following output.
Since isDataProcessed
is false
, the processData
function never runs and you never see "Processing 5" in the console. This shows that the expression only evaluates what is needed to get the result.
What is a generator?
A generator is a pull stream in JavaScript. This means its a special kind of function where you can pause execution and resume it later.
The Generator
 object is returned by a generator function and it conforms to both the iterable protocol and the iterator protocol.
Apart from the .next()
method, generators also have .return()
and .throw()
.
.return()
- The.return()
method terminates the generator's execution and returns the specified value, also triggering anyfinally
blocks..throw()
- The.throw()
method allows you to throw an error inside the generator at the point of the last yield, which can be caught and handled or allow the generator to clean up through afinally
block. If uncaught, it stops the generator and marks it as done.
You can also pass in numbers or any other value to generators when you call next()
with an argument.
Try to predict what will log out and when in the following example.
This example demonstrates how the generator function moreNumbers
manipulates and yields values based on the input it receives during the sequence of .next()
calls.
Take look at the output and check your prediction.
Let's breakdown of each step of the moreNumbers
generator function, so you understand it fully.
Step | Code Line | Console Output | Explanation |
---|---|---|---|
1 | const it2 = moreNumbers(40) | Initializes the generator with x set to 40 . | |
2 | console.log(it2.next()); | { value: 42, done: false } | Generator starts and logs x as 40, then yields 42 (x + 2 ). |
3 | console.log(it2.next(2012)); | { value: 2052, done: false } | Resumes with y as 2012, logs y , and yields 2052 (x + y ). |
4 | console.log(it2.next()); | { value: undefined, done: true } | Resumes, logs z as undefined (no new input), and finishes. |
Use Cases for Generators
There are three main uses cases for generators.
- Lazy evaluation - generate data on demand or process large or infinite data sets.
- Asynchronous programming - handle asynchronous operations.
- Iterators - allowing to stop in between steps for complex flows.
Earlier, you saw an example of reading a file from disk as a push stream. Below is how you would write the data reading using a generator to turn it into a pull stream.
Real-World Examples
Sagas are a prime example of handling asynchronous I/O operations. But you're going to learn how to use sagas in a future article, in a series of articles on Redux.
And then generally you use generators when you want to be in control WHEN to get a value.
Take a look at this test example.
In this test, you define a roleGenerator
to sequentially provide a list of roles for users within an organization. This approach allows the test to dynamically assign each user a unique role from a predefined list as part of a role management feature in a UI.
The reason a generator - as opposed to an array - was used for this example is that the position of the main user in the test is unknown in the sortedUsers
array and since a generator is a pull stream you can get the role values on demand.