Generators are a new feature introduced in ES6, and as I’ve promised in the article about async/await last week, we will cover them today.
Iterators
In JavaScript an iterator is an object that provides a next() method which returns the next item in the sequence.
The first concept to understand here is the iterator. You might have already taken advantage of their existence, not even knowing. This is because in JavaScript some built-in types have a default iteration behaviour (they are iterable). For an object to be iterable, it needs to implement the @@iterator method as the value of Symbol.iterator property.
1 2 3 4 5 6 7 8 |
const arrayOfNumbers = [1, 2, 3]; const iterator = arrayOfNumbers[Symbol.iterator]; console.log(iterator); // a function here for(let number of arrayOfNumbers) { console.log(number); // 1, 2, 3 } |
Iterators implement the next() method that returns an object containing two properties: value and done. What actually happens here, is that in this loop, the iterator.next() is called as long, as it has not returned a property done: true .
1 2 3 4 5 6 7 8 |
const arrayOfNumbers = [1, 2, 3]; const iterator = arrayOfNumbers[Symbol.iterator](); let item = iterator.next(); while(!item.done) { console.log(item.value); item = iterator.next(); // 1, 2, 3 } |
Generator functions
These are special functions that return a generator object implementing a next method (it means that they are iterators). To create one, you just need to mark a function with an asterisk. Generator functions are sometimes referred to as pausable. This is caused by the fact, that if you use a yield keyword in the function body, it is going to be stopped at that point.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function* generatorFunction() { let x = 0; console.log(x); yield; console.log(++x); yield; } const iterator = generatorFunction(); iterator.next(); // 0 iterator.next(); // 1 iterator.next(); // nothing logged |
Note that the generator function that we wrote here didn’t exactly run when called with const iterator = generatorFunction(); , but the iterator was returned. When the next method was called, our generator function ran till the yield keyword and then stopped. When next was called again, the function resumed and reached the end, so the third call of the next method didn’t cause the generator function to run again.
Return value of the next function
We can go a little further and pass a value along with a yield keyword. It is going to be included in a return value of the next method.
1 2 3 4 5 6 7 8 9 10 11 |
function* generatorFunction() { yield 1; yield 2; yield 3; } const iterator = generatorFunction(); iterator.next(); // {value: 1, done: false} iterator.next(); // {value: 2, done: false} iterator.next(); // {value: 3, done: false} iterator.next(); // {value: undefined, done: true} |
Return statement
Since this is a function, after all, it can also return a value using a return keyword;
1 2 3 4 5 6 7 8 9 10 11 |
function* generatorFunction() { yield 1; return 4; yield 2; yield 3; } const iterator = generatorFunction(); iterator.next(); // {value: 1, done: false} iterator.next(); // {value: 4, done: true} iterator.next(); // {value: undefined, done: true} |
It ends the generator function (as it would be expected of a return statement) and sets done to true.
Passing arguments to a next method
If you pass an argument to the next method call, it is going to be a return value of the yield statement. Time for a more complex example:
1 2 3 4 5 6 7 8 9 10 11 |
function* generatorFunction(x) { const y = (yield "I'm here!") * x; const z = yield y; return z * x; } const iterator = generatorFunction(2); iterator.next(); // { value: "I'm here!", done: false } iterator.next(3); // { value: 6, done: false } iterator.next(4); // { value: 8, done: true } |
- iterator.next(); The generator function starts when the next method is called for the first time
- (yield "I'm here!") Execution stops on the first yield, and the value “I’m here!” is returned
- iterator.next(3) causes the expression (yield "I'm here!") to be replaced by an argument passed to the second next method, which is 3
- yield y causes the function to stop, returning a value of a y variable, which is 3 * 2 === 6
- iterator.next(4) makes the function run again, replacing yield y with a number 4
- return z * x returns a value 4 * 2 === 8
Implementing async/await
In the previous article, we talked about async/await. I’ve mentioned there, that async/await is a syntactic sugar over generators. Hopefully, you now know a thing or two about generators. Why not implement our own async/await, then?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function run(generatorFunction) { const iterator = generatorFunction(); (function handleNext(value){ const next = iterator.next(value); if (next.done) { return next.value; } else { return Promise.resolve(next.value) .then( handleNext, err => Promise.resolve(iterator.throw(err)).then(handleNext) ); } })(); } run(function *(){ try { const usersResponse = yield fetch(`${apiUrl}/users`); console.log(usersResponse); } catch (err) { console.error(err); } }); |
In this example, I’ve created a function that takes a generator function as an argument – it works a bit like async keyword. First, it creates an iterator out of our generator function. Then, the handleNext function is called: it gets the value of a next method. Since we passed a promise with the yield keyword, it is now in the next.value property and handleNext function is going to resolve that promise. When the promise is resolved, the function handleNext runs again (thanks to the then method). If the value of done is true, it simply returns the value and its work is done. As we know, this means that our generator function came to an end.
We also need to allow try/catch block to take care of the error handling. This is done with this line:
err => Promise.resolve(iterator.throw(err)).then(handleNext)
In case of an error, we use a Generator.prototype.throw method.
The throw() method resumes the execution of a generator by throwing an error into it and returns an object with two properties done and value.
With it, the error will be propagated to the try/catch block. Thanks to all that, we have our own async/await functionality!
Summary
Generators are a very interesting and powerful functionality that ES6 brought us and are used under the hood more often than you might have guessed. With that being the case, it is definitely worth looking into, since I think it is imporant to understand the core of the JavaScript language. This is something I highly encourage you to focus on.