Generator functions in JavaScript

JavaScript Generators are a special type of function which lets you suspend the function execution and then come back later and resume the execution.

We can define a generator function with the function keyword followed by an asterisk (function*). Currently, there is no such option available for arrow functions to create generator functions.

The generator function provides a powerful keyword called yield which pauses the function execution and returns the yield expression value.

function* generatorFn() {
    console.log('start');
    yield 1;
    console.log('After yield 1');
    yield 2;
    console.log('end');
}

The generator function doesn’t execute right away when we invoke it. Instead, it returns a Generator object which contains three utility methods.

  • next()
  • return()
  • throw()

The next() method

When we call the next() method, the Generator function executes until the next yield expression. This method returns an object containing a value property that holds the yielded value and a done property that holds a boolean value which indicates if the function has finished its execution or not.

function* generatorFn() {
    console.log('start');
    yield 10;
    console.log('After yield 10');
    console.log('X', yield 20);
    console.log('end');
}

const gen = generatorFn();
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());

/*
output:

start
{ value: 10, done: false }
After yield 10
{ value: 20, done: false }
X undefined
end
{ value: undefined, done: true }

*/

First next() call – function will execute till first yield expression yield 10. Therefore, “start” will be printed and an object will be returned by yield 10 containing properties value as “10” and done as false.

Second next() call – function will start the execution from the second line (without yield expression) till the next yield expression yield 20.  Remember when execution pauses on a line, it only executes the yield expression and the rest of the statement will execute in the next next() call. So, here on the second next() call, only yield 20 will execute and pause, and the rest console statement will execute in the next iteration.

Third next() call – function will start its execution from the fourth line, executing the console statement and replacing yield 20 with undefined (as we passed no argument in next(). we’ll learn more about it later). As there is no more yield expression left, execution will continue till the end and return the object containing value as undefined and done as true.

Once the execution is finished, call to next() will always return the object with properties value as undefined and done as true.

Placing return statement inside generator function

If we have a return statement inside the generator function, then the return statement will finish the execution and the returned object will have value as the returned value and done as true.

function* generator() {
    yield 1;
    return 5;
}

const gen = generator();
console.log(gen.next());
// { value: 1, done: false }
console.log(gen.next());
// { value: 5, done: true }

If we place the return statement before any yield expression, the return statement will finish the execution and the yield expression will be ignored.

function* generator() {
    yield 1;
    return 5;
    yield 10;
}

const gen = generator();
console.log(gen.next()); 
// { value: 1, done: false }
console.log(gen.next()); 
// { value: 5, done: true }
console.log(gen.next()); 
// { value: undefined, done: true }

Passing value in next() method

We can call the next() method with an argument, which will replace the last executed yield expression where the execution was paused with the argument value.

function* generator() {
    yield 10;
    const third = yield 20;
    yield third;
}

const gen = generator();
console.log(gen.next()); 
// { value: 10, done: false }
console.log(gen.next(55)); 
// { value: 20, done: false }
console.log(gen.next(99)); 
// { value: 99, done: false }
console.log(gen.next()); 
// { value: undefined, done: true }

When the first next() is called, yield 10 executed and execution was paused there.

When next(55) is called, it replaced the last yield expression i.e. yield 10 with “55” (We are not using this value here so we won’t see a difference), then executed yield 20 and execution paused there.

When next(99) is called, it replaced the last yield expression i.e. yield 20 with “99” and assigned value “99” to variable “third”. Then yield third executed and returned object with properties value as “99” and paused execution there.

Finally, when the last next() is called, code after the last yield executed, here there was no code available after the last yield so it simply finished the execution and returned the object with done as true.

The return() method

When we call the return() method, it will finish the function execution there itself where the execution was paused. It will return an object with properties value as undefined and done as true. We can also pass a value in the return() method which will replace the value property in the returned object.

function* generator() {
  console.log("start");
  yield 10;
  console.log("after yield 10");
  yield 20;
  console.log("end");
}

const gen = generator();
console.log(gen.next());
console.log(gen.return(55));
console.log(gen.next());

/*
output:

start
{ value: 10, done: false }
{ value: 55, done: true }
{ value: undefined, done: true }

*/

The throw() method

This method basically stops the execution there itself and throws an exception. This method won’t return any object like next() and return() does. We can pass any error information to this method for debugging purposes.

function* generator() {
  console.log("start");
  yield 10;
  console.log("after yield 10");
  yield 20;
  console.log("end");
}

const gen = generator();
console.log(gen.next());
console.log(gen.throw(new Error('Some error occured')));
console.log(gen.next());

/*
output:

start
{ value: 10, done: false }
Uncaught Error: Some error occured

*/

Generator object as iterator

The generator object is iterable, so we can easily loop through it and use the values returned by each yield expression.

function* generator() {
  yield 25;
  yield 45;
  yield 65;
}

// using for...of loop
for (let value of generator()) {
  console.log(value);
}
// 25
// 45
// 65

// Array.from()
console.log(Array.from(generator())); 
// [25, 45, 65]

// spread operator
console.log([...generator()]); 
// [25, 45, 65]

Using the yield* keyword with iterables

The yield* keyword iterates over the iterable operand and yields each value returned by it. This keyword must be used with iterables only, else it will throw an error.

Some of the built-in iterables are Array, Map, String, Generators, etc.

Example with string and array

function* generator() {
  yield 10;
  yield [20, 30];
  yield* [40, 50];
  yield 'html';
  yield* 'js';
}

const gen = generator();

console.log(gen.next().value); // 10
console.log(gen.next().value); // [20, 30]
console.log(gen.next().value); // 40
console.log(gen.next().value); // 50
console.log(gen.next().value); // html
console.log(gen.next().value); // j
console.log(gen.next().value); // s

Example with generator

// print a series of numbers like 1223334444...n(n times)

function* repeat(n) {
  let count = 1;
  while (count <= n) {
    yield n;
    count++;
  }
}

function* generateSequence(n) {
  let count = 1;
  while (count <= n) {
    yield* repeat(count);
    count++;
  }
}

const gen = generateSequence(3);
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(gen.next().value); // 3
console.log(gen.next().value); // 3
console.log(gen.next().value); // undefined

Multiple instances of generator function

We can create multiple instances of generators and they don’t interfere with each other’s execution and work independently. Let’s see one example.

function* generator() {
  yield 10;
  yield 20;
}

const gen1 = generator();
const gen2 = generator();

console.log(gen1.next()); 
// { value: 10, done: false }
console.log(gen1.next()); 
// { value: 20, done: false }
console.log(gen2.next());
// { value: 10, done: false }
console.log(gen1.next()); 
// { value: undefined, done: true }
console.log(gen2.next()); 
// { value: 20, done: false }
console.log(gen2.next()); 
// { value: undefined, done: true }

You may also like

Leave a Reply

Your email address will not be published. Required fields are marked *