Generator Functions in Javascript

Generator functions are a special type of function that can be paused and resumed, allowing for more control over the flow of execution. Here are the key things to know about generator functions.

A generator function is defined by an * at the end of the function keyword.

function* generatorFunction() {}
//OR
const generatorFunction = function*() {}

A generator function returns a Generator object. The Generator object returned by the function is an iterator. An iterator object has a method called next() which is used to iterate through a sequence of values. The next() method returns an object with value and done properties. Value would be the current value that was returned and done is a boolean that indicates if the iterator has stopped or if it’s still running.

Best way to understand the above is to see an example:

// Declare a generator function with a single return value
function* generatorFunction() {
  return 'Hello, World!'
}

// Assign the Generator object to generator
const generator = generatorFunction();

// Call the next method on the Generator object
generator.next(); //Output: {value: "Hello, World!", done: true}

The value returned from calling next() is Hello, World!, and the state of done is true. Because this value came from a return that closed out the iterator. Just like how in a normal function, where you write a return statement the compiler will exit that function so it’s the same here.

But then what makes generator functions different? Generators introduced a new keyword called yield. A yield can pause a generator function & return a value. Here is an example first:

// A generator function with multiple yields and a return
function* generatorFunction() {
    yield 'Value 1'
    yield 'Value 2'
    yield 'Value 3'
    return 'Value 4'
}

const generator = generatorFunction()

// Call next four times
generator.next() // {value: "Value 1", done: false}
generator.next() // {value: "Value 2", done: false}
generator.next() // {value: "Value 3", done: false}
generator.next() // {value: "Value 4", done: true}

In the above example, we’re pausing the generator function 3 times with different values, and returning a value at the end. Then we’ll assign our Generator object to a generator variable.

Then we call next() on the generator function, and it’ll pause every time it encounters a yield. done will be false after each yield, indicating the generator has not finished. Once it encounters a return, or there are no more yields encountered in the function, done will flip to true, and the generator will be finished.

The return is not a mandatory thing for a generator function. If you don’t write it then the done the value will remain false for 4 iterations and then it’ll be true on the fifth iteration i.e. {value: undefined, done: true}.

To sum up the above theory, yield pause until a value is returned and then go to the next command and return finishes the function.

Just like an Array we can also loop through a Generator object.

for (const value of generator) { console.log(value) }

//Output:
// Value 1
// Value 2
// Value 3

You can also create an array with the values returned from a generator object:

const values = [...generator];
console.log(values)

//Output:
// ["Value 1", "Value 2", "Value 3"]

You might have noticed that both for...of and ... (spread operator) are not factoring in the Value 4 which using return and it’s only considering the values it got with yield.

We can also force stop a generator. See an example below:

function* generatorFunction() {
  yield 'Neo'
  yield 'Morpheus'
  yield 'Trinity'
}

const generator = generatorFunction()

generator.next()
generator.return('There is no spoon!')
generator.next()

// Output
// {value: "Neo", done: false}
// {value: "There is no spoon!", done: true}
// {value: undefined, done: true}

You can even keep the return empty like return() that would also force the Generator object to complete and ignore any other yield keywords.

This type of behavior is useful when we’re making let’s say cancel a web request when the user wants to perform a different action, as it is not possible to directly cancel a Promise.

If the body of the generator has a way to catch errors, you can use throw() method to throw an error into the generator. Upon throwing an error, the generator gets terminated as well.

function* generatorFunction() {
   try { yield 'Value 1'
        yield 'Value 2'
      } catch (error) { console.log(error)}
}

const generator = generatorFunction()

generator.next()
generator.throw(new Error('This is an error!'))

// Output
// {value: "Value 1", done: false}
// Error: This is an error!
// {value: undefined, done: true}

So there are three methods of Generator object:

  • next() – Returns the next value in a generator
  • return() – Returns a value in a generator and finished the generator
  • throw() – Throws an error and finished the generator

Similarly, a Generator object can be in one of these possible states:

  • suspended Generator has halted execution but has not terminated i.e. next()
  • closed Generator has terminated by either encountering an error i.e. throw(), returning i.e. return(), or iterating through all the values i.e. yield

We can also make generator function nested by using yield* which required a generator function as well. This is how you call a generator function from another :

function* delegate() { 
  yield 'Value 3'
  yield 'Value 4'
}

function* begin() {
  yield 'Value 1'
  yield 'Value 2'
  yield* delegate()
}

const generator = begin()

for (const value of generator) {
  console.log(value)
}

// Output
// Value 1
// Value 2
// Value 3
// Value 4

Infinite data streams:

One of the most important features of generators is their ability to work with infinite data streams and collections. This means it’ll be a very useful scenario like if you’re implementing infinite scrolling etc. Let’s create a program to understand this.

We’ll write a program that will print Fibonacci numbers.

// Create a generator function
  function* fibonacci() {
    let prev = 0;
    let next = 1;

    yield prev
    yield next

    // Add previous and next values and yield them forever
    while (true) {
      const newVal = next + prev
      yield newVal

      prev = next
      next = newVal
    }
  }

  const fibGenerator = fibonacci()

  // Print first five fib
  for (let i = 0; i < 5; i++) {
    console.log(fibGenerator.next())
  }
  console.log("===========")
  //Print the next 5
  for (let i = 0; i < 5; i++) {
    console.log(fibGenerator.next())
  }

// Output show in the screenshot

In a general scenario, it should print 0,1,1,2,3 again but instead, it starts from where it left because we didn’t use any return() or throw().

Just like normal functions, we can also pass arguments to generator functions.

function* generatorFunction(value) {
  while(true) {
    value = yield value * 10
  }
}

// Initiate a generator and seed it with an initial value
const generator = generatorFunction(0)

for (let i = 0; i < 5; i++) {
  console.log(generator.next(i).value)
}

// Output
// 0
// 10
// 20
// 30
// 40

I hope this gives you a general idea of what generators are and you can always explore more: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*?retiredLocale=my

Leave a comment

Design a site like this with WordPress.com
Get started