The Basics of JavaScript Generators

A quick look at one of ES6’s best features

ES6 generator functions are those that can stop their execution in the middle and resume execution further from the same point. These functions do not return a single value; instead, they’re capable of returning multiple values. They’re based on the concept of iterators.

Life Without Generator Functions

Below is a simple function in JavaScript without generators. A normal JavaScript function starts executing as soon as it’s invoked. These functions run to completion before any other code can execute.

We usually expect that the function will finish its execution before any other JavaScript function can run. A normal function starts execution and halts only when the execution is complete.

A function can stop execution in one of the following scenarios:

  1. The compiler reaches the end of the function.
  2. It encounters the return keyword.
  3. The function throws an error or exception.

Generators: We Are Different!

In the case of generators, these functions are capable of returning multiple values, and they may not execute completely. Function execution can be triggered in parts. It can stop execution in the middle and resume another task and, after a while, resume the function from where it left off. Let's see a basic example of generators before we proceed further.

Generator functions can pause and be resumed one or multiple times, which makes them interesting and very different from normal functions. While the function is paused, it allows other functions and code to execute. By default, generators are asynchronous.

The code above contains a simple generator implementation. When a generator is called, it returns an iterator. The * after the function keyword indicates that it’s a generator and an iterator is returned from the function. This iterator won’t fetch results by itself. It needs to be triggered from outside.

Generators: Tell Me When I Can Start

Once the iterator has been created, we need to trigger this iterator to fetch data from the generator. We can continue execution on the function by calling the next method on the returned iterator to retrieve the value from the generator function. Each time the next() function is called on the iterator, it executes the function until it reaches the next yield keyword. After each yield, it stops processing further.

Once the execution halts, we need to re-trigger the next function on the iterator. When the next function is triggered on the iterator, it resumes the execution until the next yield is encountered and gives 2 as output.

Generators: Don’t Tell Me to Stop

ES6 generator functions cannot be interrupted from outside. Until and unless yield is encountered during execution, generators cannot be halted. If the generator function does not contain any yield keywords, it will behave like a normal function which executes to completion.

Getting Values Back From Generators

In the program below, it continues executing the generator function when next() is encountered on the returned iterator. It starts executing the function and halts on line 3 when the first yield is encountered. The yield keyword can return a value to the calling next() function. In this case, the first yield keyword would return a value 1 along with the flag signifying whether the generator execution is complete or not.

Since the generator is still not complete, it will return the flag done value as false. yield returns a key/value pair containing value and done as the keys.

So the output returned is: { value: 1, done: false }

1 represents the value which is paired with the yield keyword and since the generator can yield further values, the done property is still false.

The next function can be called multiple times on the iterator, the flag done will indicate whether the generator is complete or not. Once the generator has finished execution, all further output will show the value of done flag to be true, and the value field to be undefined.

Calling the next function will resume the generator’s execution until the next yield is available. On line 24, all the yield keywords are exhausted, so in this case, the returned value would be undefined (since functions don’t return anything) with the done flag set to true.

Using the Return Keyword in Generators

A generator function can have a return statement. As soon as the generator function encounters the return keyword, it updates the done flag to true, and even if we have more yield keywords in the function, the generator will be considered complete.

Once the return keyword is executed, all other yields would return undefined with the done flag set to true.

Passing Values to the “Next” Function

We’ve seen that when we pass a value to yield keyword, it returns the value to the calling function next. However, generators enable two-way communication. The next function can pass data to the generators too.

In code below, you can see that in line 15, we pass a parameter to the next function. Now let’s understand how this line of code would execute.

Below are the events when the first call to next() is made:

  1. When next is called the first time on the iterator, the generator starts execution.
  2. The generator code executes until encountering the first yield keyword.
  3. It returns what is passed to the yield function.

Output: { value: 1, done: false }

When next(10) is invoked:

  1. The parameter passed is substituted in place of yield 1.

So effectively, line 3 in the generator becomes let x = 10.

2. It further executes the function until encountering the next yield.

3. It substitutes the value of x with 10 and returns the resulting value (10 +1).

Output: { value: 11, done: false }

On the following call of next():

  1. Since there is no further yield keyword, it returns undefined as the return value.
  2. And since the generator is complete, the done flag is set to true.

Output: { value: undefined, done: false }

Working With for … of Loops

Since the generator functions return iterators, these iterators can be used inside a for of loop. The iterator will iterate through all the values present in the generator function. Parameters passed to the yield keyword will be passed as a value to the iterator.

I hope you’re now more comfortable with using generator functions. Generators are amazing and can resolve various use case scenarios. Try to find scenarios in your current application where generators will make things easier. I hope this article helped you!