Click to share! ⬇️

es6 generators

Generators in ES6 are a special kind of function that return an iterator. They are quite a bit different than your standard run of the mill function in JavaScript however. Generators can pause themselves as they are running, and return multiple values as execution pauses and resumes. You use an iterator to call a generator multiple times. Let’s look at some examples to better understand how generators work.


Defining a Generator Function

function *generateit() {
    yield 200;
    yield 300;
}

let iter = generateit();
console.log(iter.next());

// Object { value: 200, done: false }

Notice the asterisk just in front of the generateit() function. This * symbol indicates that we are dealing with a generator function. In addition to this, we can see these new statements inside the function that make use of the yield keyword. The function yields a value of 200, and it also yields a value of 300. Once both values are yielded, the function will exit. Next up, we actually run the generateit() function, and it assigns an iterator in a paused state to the variable iter. When we log out the value of iter.next(), we see the result of Object { value: 200, done: false }. So in essence, the function paused at the first yield statement. The following is the result of multiple calls to iter.next():

let iter = generateit();
iter.next();
console.log(iter.next());

// Object { value: 300, done: false }
let iter = generateit();
iter.next();
iter.next();
console.log(iter.next());

// Object { value: undefined, done: true }

By this code and output, we see that a generator completes just like an iterator does.


Yielding Indefinitely

In this example, we’ll create a generator function that can yield indefinitely. In other words, it can never be exhausted. Rather than having hard coded values like our first examples of a generator, here we will use some logic inside the generator function to set a starting point, and then an increment operation on every yield.

function *generateit() {
    let nextNum = 400;
    while (true) {
        yield(nextNum++);
    }
}

let iter = generateit();
iter.next();
console.log(iter.next().value);

// 401

Using a Generator in a for of loop

Since a generator is controlled by an iterator, you can easily make use of generator functions inside for of loops. In this example below, we have an interesting construct in that we actually call generateit() right inside the loop. We also add a condition inside to prevent this for of loop from going on forever.

function *generateit() {
    let nextNum = 400;
    while (true) {
        yield(nextNum++);
    }
}

for (let num of generateit()) {
    if (num > 403) break;
    console.log(num);
}

// 400
// 401
// 402
// 403

A closer look at Yielding

Here we will rewrite our generator function so that it does not yield a specific value. If we try to call this generator function we get a result of Object { value: undefined, done: false }

function *generateit() {
    yield;
}

let iter = generateit();
console.log(iter.next());

// Object { value: undefined, done: false }

A new way to yield

The following generator function makes use of yield in a new way. Inside the generator function we now have a variable that gets initialized to yield. Right after this, we’ll just log out a string with the value of result contained in it.

With this in place, we call the generator and assign the iterator to iter. The first call to iter.next() is what starts up the generator so to speak, which causes the generator to immediately yield. At this point, you can now call iter.next() and pass in a value. This value gets set to the result variable. So when we pass in 777, we can see that The result is 777 is what gets logged out to the console.

function *generateit() {
    let result = yield;
    console.log(`The result is ${result}`)
}

let iter = generateit();
iter.next();
iter.next(777);

// The result is 777

Let’s wrap the second call to iter.next() in a console.log() statement. The first line still shows the same The result is 777 from the generator, however we now also get to inpsect the iterator object. That object has value set to undefined, and done set to true. This is because once we passed 777 to iter.next(), there was no further yield in the generator – so at that point, it is done.

function *generateit() {
    let result = yield;
    console.log(`The result is ${result}`)
}

let iter = generateit();
iter.next();
console.log(iter.next(777));

// The result is 777
// Object { value: undefined, done: true }

Using yield in place of an expression

We can use the yield keyword in places where you might be more likely to see an expression. Let’s see how this might work.

function *generateit() {
    let arrayOfYields = [yield, yield, yield];
    console.log(arrayOfYields);
}

let iter = generateit();
iter.next();
iter.next('PHP');
iter.next('JavaScript');
iter.next('Linux');

// Array [ "PHP", "JavaScript", "Linux" ]

This example sets up a new array, and then initializes it’s value to 3 yield keywords. After that, we want to log out the arrayOfYields to see what it contains.

With this in place, we call the generator, then call iter.next() four times. Once to initialize, and then three more times passing in a value to be placed into the array on each call. Inspecting the console shows us the final array of [ “PHP”, “JavaScript”, “Linux” ].


yield precedence is very low

When using the yield keyword in an expression, you should wrap it in parenthesis as a good practice. If you don’t, you might not get the results you expect. In the following example, we want to make use of multiplication in our generator function. We wrap the yield in parenthesis to make things work, otherwise yield is ignored and the expression fails.

function *generateit() {
    let result = 5 * (yield);
    console.log(result);
}

let iter = generateit();
iter.next();
iter.next(2);

// 10

Yielding a single value and an array in the same generator

Here we will yield the value of 77, and then an array which has 3 different string values contained within. Once again, we trigger the generator, then make successive calls to iter.next().value while logging out the result. On the first call to iter.next().value we get the value of 77. On the second call to iter.next().value, we get the full array in one shot. The final call to iter.next().value gives a result of undefined since the iterator is exhausted at this point.

function *generateit() {
    yield 77;
    yield ['Node', 'Angular', 'React']
}

let iter = generateit();
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);

// 77
// Array [ "Node", "Angular", "React" ]
// undefined

Yielding each value of an array one at a time

If you would like to log out each value individually, you can make use of iterator delegation. When we put the asterisk just after the yield keyword like we see below, this means that the yield expects something that is iterable. In this case it is an array, and that is definitely iterable. With this construct inside of a generator, you will find that this internal iterator temporarily replaces the iterator for *generateit(). So when you use yield*, you are delegating another iterator to the generator. Once that iterator is fully consumed, the previous iterator will take over again.

function *generateit() {
    yield 77;
    yield* ['Node', 'Angular', 'React']
}

let iter = generateit();
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);
console.log(iter.next().value);

// 77  
// Node  
// Angular  
// React  
// undefined

throw and return in Generators

It is possible to get a finer degree of control over iterators by making use of the throw and return keywords. Let’s inspect a few examples to see how they operate.

The following snippet has a generator that yields three values. Notice that they exist in a try / catch block. Following the generator definition, we kick off the generator as normal, then log out a few values. The first value logged out is that of ‘Fruit’. After this however, we make a call to iter.throw(), and pass in a message of ‘Hey now’. The result of his call is that we receive an object with value set to undefined, and done set to true. What this means is that an exception was thrown, and handled by the catch block. We don’t have any logic in the catch block, so it kind of fails silently. On the final call to iter.next(), the generator has already completed so we get the object with value set to undefined and done set to true.


throw in generators

function *generateit() {
    try {
        yield 'Fruit';
        yield 'Coffee';
        yield 'Oatmeal'
    } catch (e) {

    }
}

let iter = generateit();
console.log(iter.next().value);
console.log(iter.throw('Hey now'));
console.log(iter.next());

// Fruit
// Object { value: undefined, done: true }
// Object { value: undefined, done: true }

If we omit the try catch block from the generator, the first value still gets printed out. When we hit iter.throw(), and exception is thrown but the generator has no catch logic so it terminates the script entirely. This is why we don’t see any object on the final call to iter.next(). The takeaway is, if you want to call iter.throw() in your code, you should ensure you have a try catch block to handle it.

function *generateit() {
    yield 'Fruit';
    yield 'Coffee';
    yield 'Oatmeal'
}

let iter = generateit();
console.log(iter.next().value);
console.log(iter.throw('Hey now'));
console.log(iter.next());

// Fruit
// Uncaught Exception: Hey now

return in generators

Making use of the return function is a way to clean up your generators. Below we make use of our familiar generator function, kick it off, then log out the first value. After this, we see something new in console.log(iter.return(‘Hey now’)); When this is called, we can see in the output that we get an object with value set to ‘Hey now’ and done set to true. The final call to iter.next() gives us the familiar finished iterator object with value set to undefined and done set to true. Calling return on an iterator is a nice way to wrap up the iterator and complete it’s execution. Whatever the parameter is that gets passed to iter.return() becomes the value of the returned object. Since we called return, we never make it to ‘Coffee’ or ‘Oatmeal’ in the generator. The return function finished it.

function *generateit() {
    yield 'Fruit';
    yield 'Coffee';
    yield 'Oatmeal'
}

let iter = generateit();
console.log(iter.next().value);
console.log(iter.return('Hey now'));
console.log(iter.next());

// Fruit
// Object { value: "Hey now", done: true }
// Object { value: undefined, done: true }

Example of a Fibonacci sequence using a generator

Here is an example of the common Fibonacci sequence which makes use of a generator to produce the result. Note that this puppy is an infinite sequence, so only make use of the next() iterator, or you could crash your browser.

function *fibonacci() {
    let a = 0, b = 1;
    yield a;
    yield b;
    while( true ) {
        [a, b] = [b, a+b];
        yield b;
    }
}

let fibs = fibonacci();
console.log(fibs.next());
console.log(fibs.next());
console.log(fibs.next());
console.log(fibs.next());
console.log(fibs.next());
console.log(fibs.next());
console.log(fibs.next());
console.log(fibs.next());

// Object { value: 0, done: false }  
// Object { value: 1, done: false }  
// Object { value: 1, done: false } 
// Object { value: 2, done: false }  
// Object { value: 3, done: false }  
// Object { value: 5, done: false }  
// Object { value: 8, done: false } 
// Object { value: 13, done: false }

Using the spread operator with Generators

We recently learned about spread operators and how they can be used to consume an iterable. This means we can use the spread operator with generators. Let’s see an example of that. Here we have a generator function named seasons. It yields four different values as we might expect. Notice that when we launch our generator, we first make a call to theseasons.next() which gives us the value of Object { value: “Spring”, done: false }. After this, we make use of the spread operator with the iterable like so: [...theseasons]. For this we get all the remaining yield values of the generator function as an array, pretty cool! Finally, we call theseasons.next() again just to confirm that yes, our generator is exhausted and it has reached a finished state.

Special note: You never want to make use of the spread operator on a generated iterable if the generator is an infinite sequence. The reason is that this will create a never ending loop of yields which will consume all memory and crash the browser.

function *seasons() {
    yield 'Spring';
    yield 'Summer';
    yield 'Fall';
    yield 'Winter';
}

let theseasons = seasons();
console.log(theseasons.next());
console.log([...theseasons]);
console.log(theseasons.next());

// Object { value: "Spring", done: false }
// Array [ "Summer", "Fall", "Winter" ]
// Object { value: undefined, done: true }

ES6 Generators Summary

A generator in ES6 is a special kind of function that returns an iterator. There are a few differences between generator functions and regular functions. We list them here.

  • Generator functions create and return iterators
  • There is an asterisk after the function keyword to denote a generator
  • You can use the yield keyword in the created iterator function. By writing yield ‘Bazinga’, the iterator returns the object of { value: ‘Bazinga’, done: false }
  • The yielded result is the next value of the iteration process. Execution of the generator function is paused at the point of yielding. Once a data consumer asks for another value, execution of the generator function is resumed by executing the statement after the last yield.
  • You can use the return keyword to end the iteration.

Iterator functions are a tricky topic in ES6, and it will take some time to become familiar with them. As ES6 matures and we see it more out in the wild, more practical applications of generators will become available to see how to best make use of them.

Click to share! ⬇️