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 -> Hello
To 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 -> Hello
If 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 -> World
A 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 World
If 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 wrong
We 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 ->