JavaScript ES6 Generators

Generators are a new concept in ES6. Generator functions let you pause and resume functions. Generators take some getting used to but can help with writing asynchronous code. Using generators, you can pause functions and wait for asynchronous activity to complete.

Generator functions vs regular functions

Regular functions run to completion. There is no pause or break during execution. With generator functions, you can pause execution and pass in new values. By using the yield keyword, a generator function can be paused, passed new values, and resumed at a later time.

Syntax

Generator functions have almost identical syntax to regular functions. The only real difference is the * used in the definition:

function *myGen(){
  //valid
}

function* myGen(){
  //valid
}

function*myGen(){
  //valid
}

How generator functions work

Generator functions use iterators to pause and resume functions. Generators use the yield keyword to pause execution internally. When a generator function comes across yield, it returns an iterator object like this:

{value: x, done: bool}

This iterator object has a property value with the current yielded value and done indicating the completion of the iteration object:

function *myGen()  {
  let a = yield 'the first yield returned'
  console.log(a)
  let b = yield 'the second yield returned'
  console.log(b)
  return "all done"
}

let generator = myGen()

generator.next()

//returns {value: 'the first yield returned', done: false}

generator.next("hi")

//logs 'hi'

//returns {value: 'the second yield returned', done: false}

generator.next("bye")

//logs 'bye'

//returns {value: 'all done', done: true}

Notice how we first define a basic generator function *myGen with two yield statements. We then assign the variable generator the invoked generator function myGen().

Whenever we call next(), the function resumes until it hits the next yield clause. Notice how the first next() call returns the first yield returned. When we call next() a second time, we pass in the argument "hey" and resume execution. Notice how our passed value "hey" is then logged before the second yield is encountered. After calling next() again, the function finds no more yields and returns.

Using generators in the real world

Generators are great for working with asynchronous operations in JavaScript. They work especially well with promises to handle async activity. Specifically, you can use generators to prevent excessive promise chaining:

const makeRequest = () => new Promise((resolve) => {

  setTimeout(() => resolve('success'), 500)

})


const myGenerator = function* () {
  yield
  const response = yield makeRequest()
  // waits for promise

  yield response

}

Notice how our myGenerator function waits for the promise in makeRequest() to resolve before continuing. While we could have used then() to handle the resolved promise, this syntax eliminates the need for nested chaining and response handling.

In the real world, we would need to call next() to progress through the function. Ideally, we would want the myGenerator() function to resolve without having to call next() for each yield. Thankfully, libraries like Co.js implement wrappers that resolve generators automatically to optimize them for real world scenarios.

Conclusion

Generator functions take some getting used to but ultimately add elegance to async programming in JavaScript. With the ability to pause and resume functions, generators give you more flexibility in working with async operations and improve code readability.

Your thoughts?