More Details on Generators

Published: Thursday, October 22, 2020
Updated: Thursday, October 29, 2020

Greetings, friends! Generators are a fascinating construct in programming. In my previous article, I gave a brief introduction on what generators are and how to use them. Consider this post a follow-up discussion as I describe generators in greater detail and discuss how to avoid common pitfalls.

Quick Refresher on Generators

Generators are both iterators and iterables, since they adhere to both the iterator and iterable protocols. They allow us to pause execution of a function and return to it later. This allows us to evaluate the function lazily, so we don't end up iterating through an entire iterable such as all the elements of an array. Let's look at a simple generator function that returns a generator:

javascript
Copied! ⭐️
function *generator() {
  // First next() call begins here, before the first yield statement
  yield 1;

  // Second next() call begins here
  yield 2;

  // Calling next() a third time or more results in a value of undefined
  // and the done property is set to true
}

const g = generator();

console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: undefined, done: true }

A generator function is created by placing an asterisk * directly to the left of the function name or directly after the function keyword such as function*. You use the yield keyword to specify what value should be returned when the next method is called on the generator.

Passing values to the "next" Method

Previously, we've been passing no values to the next method, but it actually accepts one parameter. When you pass a value to the next method, it will be the result of the next yield statement run in your generator.

javascript
Copied! ⭐️
function *generator() {
  const value = yield 1;
  console.log(value);
  yield 2;
}

const g = generator();

console.log(g.next()) // { value: 1, done: false }

console.log(g.next("Pizza"))
/* OUTPUT:
Pizza
{ value: 2, done: false }
*/

console.log(g.next()) // { value: undefined, done: done }

It's important to understand where the generator starts and stops execution if you want to pass values to the next method and use them reliably in the generators. When the first invocation of the next method happens, the value passed to it is always ignored:

javascript
Copied! ⭐️
function *generator() {
  // The first invocation of next() starts BEFORE the first yield statement
  const value = yield 1;
  // The first invocation of next() ends AFTER reaching "yield 1" but BEFORE assigning the result of "yield 1" to the variable, "value"
  // The second invocation of next() starts BEFORE the assignment to "value" occurs

  console.log(value); // value = Cheese
  yield 2;
  // The second invocation of next() ends here, AFTER reaching "yield 2"

}

const g = generator();

console.log(g.next('Pizza')) // { value: 1, done: false }

console.log(g.next('Cheese'))
/* Output:
Cheese
{ value: 2, done: false }
*/

console.log(g.next('Pineapple')) // {value: undefined, done: true}

As you can see, it's meaningless to pass a value on the first invocation of the next method. It's also meaningless to pass a value to a generator that has already completed, since you will only get back a value of undefined. Keep this in mind when you want to pass values to the next method, so you can incorporate correct conditional logic or other business logic in your generator.

When we run the next method for the first time in the above example, it runs up to the yield 1 statement. However, the assignment operation that takes place when we run const value = yield 1 doesn't happen during the first next invocation. This is why we never see "Pizza" being logged. The following assignment only will occur after the second next invocation:

javascript
Copied! ⭐️
const value = 'Cheese';

Returning a Value in Generators

We can also return values in generators when they complete instead of receiving an undefined value with the done property is set to true. However, a value of undefined will be returned if you keep trying to iterate the completed generator.

javascript
Copied! ⭐️
function *generator() {
  const value = yield 3;
  return value * 4;
}

const g = generator();

console.log(g.next()); // { value: 3, done: false }
console.log(g.next(2)); // { value: 8, done: true }
console.log(g.next()); // Logs { value: undefined, done: true } because the generator can only return a value once upon completion

It's important to note that if you iterate through this generator using a for...of loop or other constructs that iterates through iterables such as Array.from(), the value in the return statement will be ignored.

javascript
Copied! ⭐️
function *generator() {
  yield 3;
  return 4;
}

const g = generator();

for (const value of g) {
  console.log(value);
}

// OUTPUT: 3
// You will not see the value of 4 appear here

The "return" Method

Generators not only have a next method, but they also have a return method as well, not to be confused with generators that return values, as described in the above section. Don't mix up these two! The return method is not the same as using a return statement inside the generator.

javascript
Copied! ⭐️
function *generator() {
  yield 1;
  yield 2;
  yield 3;
}

const g = generator();

console.log(g.next()) // { value: 1, done: false }
console.log(g.return("Pineapple")) // { value: "Pineapple", done: true }
console.log(g.next()) // { value: undefined, done: true }

When you use a generator's return method, it will automatically complete the generator and skip the other yield statements. Even if you ran an infinite while loop inside the generator, the return method will still complete the generator:

javascript
Copied! ⭐️
function *generator() {
  while (true) {
    yield 1;
    yield 2;
  }
}

const g = generator();

console.log(g.next()) // { value: 1, done: false }
console.log(g.return("Pineapple")) // { value: "Pineapple", done: true }
console.log(g.next()) // { value: undefined, done: true }

However, if you yield inside a finally clause, then you can prevent the return method from terminating the generator. The next invocation of the next method will then finish the generator with the value passed into the return method.

javascript
Copied! ⭐️
function *generator() {
    try {
        console.log('Started');
        yield;
    } finally {
        yield 'Not finished, yet!';
    }
}

const g = generator();

console.log(g.next());
/* Output:
Started
{value: undefined, done: false}
*/

console.log(g.return('Result')); // {value: "Not finished, yet!", done: false}
console.log(g.next()); // {value: "Result", done: true}
console.log(g.next()); // {value: undefined, done: true}

Infinite Generators

If you want to iterate through your generator infinitely, you can use a loop!

javascript
Copied! ⭐️
function *generator() {
  while(true) {
    yield 1;
    yield 2;
    yield 3;
  }
}

const g = generator();

console.log(g.next().value); // 1
console.log(g.next().value); // 2
console.log(g.next().value); // 3
console.log(g.next().value); // 1
console.log(g.next().value); // 2
console.log(g.next().value); // 3
// repeats forever...
danger
Unless you can escape the infinite loop, DO NOT call for...of on an infinite generator or infinite iterable as that may lead to a memory leak and cause your browser to crash and/or potentially cause your computer to freeze.

The "throw" Method

Generators also have a throw method available on their prototype. This method throws an error into the generator and resumes its execution. In the following example, we are catching the error, so the JavaScript program continues to run and the generator continues to execute.

javascript
Copied! ⭐️
function* gen() {
  while(true) {
    try {
       yield 42;
    } catch(e) {
      console.log('Error caught!');
    }
  }
}

const g = gen();

console.log(g.next()); // { value: 42, done: false }

console.log(g.throw(new Error('Something went wrong')));
/* Output:
"Error caught!"
{ value: 42, done: false }
*/

// Since this generator has a while loop, the generator can keep yielding values
console.log(g.next()); // { value: 42, done: false }

The yield* keyword

The yield* keyword behaves a bit different than yield. The yield* expression is used to delegate to another generator or iterable. Essentially, it will forward a next method call to another generator or iterable:

javascript
Copied! ⭐️
function* func1() {
  yield 42;
}

function* func2() {
  yield* func1();
}

const iterator = func2();

console.log(iterator.next().value);
// expected output: 42

We can delegate to arrays as well, since they are considered iterables:

javascript
Copied! ⭐️
function* delegateToArray() {
  yield* [1, 2, 3];
}

const g = delegateToArray();

console.log(g.next()); // {value: 1, done: false}
console.log(g.next()); // {value: 2, done: false}
console.log(g.next()); // {value: 3, done: false}
console.log(g.next()); // {value: undefined, done: true}

This lets us iterate through an array lazily, so we don't have to walk through every element of the array. This could help boost performance if you have a large array of values. Suppose you're performing complex calculations, but you want to give the JavaScript event loop time to "breathe" so that it can perform other operations like painting objects to the screen or performing AJAX requests to make API calls to a remote server. You can run through the next method dynamically about 100 times, give the event loop time to gasp for air, run through another 100 elements in the array, and repeat.

Strings can be iterated over character by character:

javascript
Copied! ⭐️
function* delegateToString() {
  yield* 'nate';
}

const g = delegateToString();

console.log(g.next()); // {value: 'n', done: false}
console.log(g.next()); // {value: 'a', done: false}
console.log(g.next()); // {value: 't', done: false}
console.log(g.next()); // {value: 'e', done: false}
console.log(g.next()); // {value: undefined, done: true}

You can even iterate over parameters in an argument passed to a generator function:

javascript
Copied! ⭐️
function* delegateToArgument() {
  yield* Array.from(arguments);
}

const g = delegateToArgument(1, 2, 3);

console.log(g.next()); // {value: 1, done: false}
console.log(g.next()); // {value: 2, done: false}
console.log(g.next()); // {value: 3, done: false}
console.log(g.next()); // {value: undefined, done: true}

Let's look at a more complex example:

javascript
Copied! ⭐️
function* g1() {
  yield* [1, 2, 3];
  return 'foo';
}

function* g2() {
  // First invocation of next() starts here
  const g1ReturnValue = yield* g1();
  console.log(g1ReturnValue) // 'foo'
  return 'bar';
}

const iterator = g2();

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

/* Output:
{ value: 1, done: false }
{ value: 2, done: false }
{ value: 3, done: false }
foo
{ value: 'bar', done: true }
{ value: undefined, done: true }
*/

Try to follow along this example piece by piece. We make two generator functions but only make an instance of the g2 generator. Then, we call next for the first time, which delegates to the g1 generator:

javascript
Copied! ⭐️
const g1ReturnValue = yield* g1();

The second and third next invocations will delegate to the array in g1 as well.

javascript
Copied! ⭐️
yield* [1, 2, 3];

The fourth next invocation will set g1ReturnValue equal to foo and log it to the console.

javascript
Copied! ⭐️
const g1ReturnValue = yield* g1();
console.log(g1ReturnValue) // 'foo'

The fifth next invocation will complete the generator. Since there's a return value in g2, we will see a value of bar and done set to true.

javascript
Copied! ⭐️
return 'bar';

Running a sixth next invocation or more tries to call a completed generator which results in a value of undefined. See! That example wasn't so bad now that you're a generator pro!

Calling the "next" Method Dynamically

Let's say we want to call the next method dynamically, say an "n" number of times. It would be a hassle to keep manually calling the next method over and over 😵. Luckily, we can just use a simple loop that calls next as many times as we want:

javascript
Copied! ⭐️
function *generator() {
  while(true) {
    yield 1;
    yield 2;
    yield 3;
  }
}

const g = generator();

function runGen(gen, runCount) {
  for (let i = 0; i < runCount; i++) {
    console.log(gen.next().value);
  }
}

runGen(g, 2);
/* Output:
1
2
*/

runGen(g, 2);
/* Output:
3
1
*/

Iterating through Generators Multiple Times

In my previous article, I mentioned that you can iterate through a generator only once. However, this only applies to iterating through a single generator instance. If we create a new instance each time we want to iterate through our generator, then it becomes possible to iterate through it "multiple times."

javascript
Copied! ⭐️
function *generator() {
  yield 1;
  yield 2;
  yield 3;
}

// Iterate through a generator instance
for (const value of generator()) {
  console.log(value);
}
/* OUTPUT:
1
2
3
*/

// Iterate through the generator a second time by using a new generator instance
for (const value of generator()) {
  console.log(value);
}
/* OUTPUT:
1
2
3
*/

If we try to iterate through a single instance, it won't work:

javascript
Copied! ⭐️
function *generator() {
  yield 1;
  yield 2;
  yield 3;
}

const g = generator();

// Iterate through a generator instance
for (const value of g) {
  console.log(value);
}
/* OUTPUT:
1
2
3
*/

// Iterate through the same generator instance
for (const value of g) {
  console.log(value);
}
// Nothing will be output here

Making a Toggle With Three or More Values

Let's have some fun. It's easy to create a toggle between two values because you can use a boolean to represent 2 states and switch between them easily:

javascript
Copied! ⭐️
let flag = true;
flag = !flag; // flag is now false

It's harder to make a toggle between three or more values. However, we can easily toggle between multiple states using an infinite generator:

javascript
Copied! ⭐️
function *generator() {
  while(true) {
    yield 1;
    yield 2;
    yield 3;
  }
}

const g = generator();

console.log(g.next().value); // 1
console.log(g.next().value); // 2
console.log(g.next().value); // 3
console.log(g.next().value); // 1
console.log(g.next().value); // 2
console.log(g.next().value); // 3

Let's use this concept to toggle the color of a square between three colors: red, green, and blue in that order.

Let's use some HTML to make a square and a button:

html
Copied! ⭐️
<div id="square"></div>
<button id="toggle-button">Toggle</button>

Next, we'll add in a little CSS:

css
Copied! ⭐️
#square {
  background-color: red;
  width: 200px;
  height: 200px;
}

#toggle-button {
  margin-top: 1rem;
}

Then, we'll add the logic needed to toggle the color of the square between red, green, and blue:

javascript
Copied! ⭐️
const square = document.getElementById('square');
const toggleButton = document.getElementById('toggle-button');

function *toggleColor() {
  while(true) {
    yield 'green';
    yield 'blue';
    yield 'red';
  }
}

const toggle = toggleColor();

toggleButton.addEventListener('click', () => {
  square.style.backgroundColor = toggle.next().value;
});

When finished, you can click on the button to toggle between the different colors. Each time you click the button, the background color of the square will be set to the next iterated value of the generator.

square cycling through red, green, and blue colors with button click

You can see the finished example on CodePen. Since the default color is red, we yield green first, blue second, and red third. If we keep clicking the button, the cycle will continue forever, since the generator never finishes due to the infinite while loop.

Using this idea, you could create a theme switcher for your website that toggles between multiple states instead of simply light and dark mode! Maybe you want a sepia theme or maybe you're more daring and want to let your customers customize the theme of your website 😃.

Conclusion

As you can tell, generators are very powerful, but you must be careful on how they are written. Generators can help improve performance of your application depending on how they're used. Generally, you shouldn't need generators, but in case you do, you are now all the wiser 😎. I hope my articles have helped generate a bunch of generator experts! 🙂