Generators are functions that can be paused and resumed arbitrarily. The normal behavior of a Javascript function would be to execute its entire body when called. A generator's body instead can be divided in blocks of code that will be executed when a certain method is called on a generator's instance.
To create a generator we add an asterisk after the function keyword.
function* createLogger () {
// your code
}When a generator is called instead of executing its body it returns an instance of itself so in order to execute the first block we can just create an instance, assign it to a variable and call the .next() method on it.
function* createLogger () {
console.log('Hello')
}
const logger = createLogger()
logger.next()
// outputs -> HelloTo pause a generator we use the yield keyword that tells the generator to stop executing. In this example we have two separated blocks so if we call the .next() method once the output will be the same as before.
function* createLogger () {
console.log('Hello')
yield
console.log('World')
}
const logger = createLogger()
logger.next()
// outputs -> HelloIf we call the method again both blocks will be executed. We can have as many blocks as we want.
...
const logger = createLogger()
logger.next()
logger.next()
// outputs -> Hello
// outputs -> WorldA generator and an instance can communicate with each other by using the yield keyword.
To output data from a generator just put it after the yield.
function* createHello () {
yield 'Hello'
}
const hello = createHello()
console.log(hello.next())
// outputs -> { value: 'Hello', done: false }The generator's output isn't just a value but an object that contains the returned value and a done variable that is set to false until the last block is executed.
...
const hello = createHello()
console.log(hello.next())
console.log(hello.next())
// outputs -> { value: 'Hello', done: false }
// outputs -> { value: undefined, done: true }If we want to pass a value from an instance to a generator instead we can do so by passing it as an argument of the .next() method when resuming execution.
function* createHello () {
const word = yield
console.log(word)
}
const hello = createHello()
console.log(hello.next())
console.log(hello.next('Hello'))
// outputs -> { value: undefined, done: false }
// outputs -> Hello
// outputs -> { value: undefined, done: true }While the generator's code seems synchronous its behavior is essentially asynchronous because when its execution is paused we can keep doing things. For this reason if something goes wrong between two .next() calls we need a way to tell the generator that an error has happened; we can do so by using the .throw() method on an instance.
For an effective error handling we can use a standard try { ... } catch { ... }.
function* createHello () {
try {
let word = yield
console.log(`Hello ${word}`)
} catch (err) {
console.log('Error', err)
}
}Considering the function above if we create an instance and we pass a value in the second .next() the block inside the try will be executed.
...
const hello = createHello()
hello.next()
hello.next('World')
// outputs -> Hello WorldIf we decide to throw and error instead the catch block will be executed with the argument value.
...
const hello = createHello()
hello.next()
hello.throw('Something went wrong')
// outputs -> Error Something went wrongWe can iterate through blocks of code inside a generator in two ways, one involves manually calling the .next() method until done: true while the other offers a more concise and elegant way.
Consider the following generator that simply yields some values.
function* createCounter () {
yield 1
yield 2
yield 3
yield 4
}Instead of calling the .next() method manually we can use a for...of and iterate through the instance.
...
const counter = createCounter()
for (let i of counter) {
console.log(i)
}
// outputs -> 1
// outputs -> 2
// outputs -> 3
// outputs -> 4
// outputs ->