Using Async/Await with Generator Functions
Mar 2, 2020 • 5 Minute Read
Introduction
With ES2015, the developer community got a lot of new features in the Javascript language. Among those features are generator functions and async/await. Generator functions give you the ability to pause and continue the execution of a program. In contrast, async/await gives you the ability to write asynchronous code without falling into "callback hell", which you risk when writing standard promises. At first glance, it may look like these features are unrelated. However, in this guide, I will show you how to use async/await on a generator function.
Generator Function Overview
The definition of a generator function is a function that can be paused and resumed at a later time, while having the ability to pass values to and from the function at each pause point.
Generator functions can run to completion, but they don't have to. Up until now, in Javascript, a function always ran until completion. Now we can write generator functions that never stop "running". I put that in quotes because it's not like a generator function is stuck in an infinite loop. It's just that maybe we don't need the rest of the function to execute.
The syntax of a generator function isn't all that different from a normal function. We need to add an asterisk somewhere between the function keyword and the name of the function. It's not important where you put the asterisk. It just needs to be before the name.
function* fetchData() {...}
// or
function * fetchData() {...}
// or
function *fetchData() {...}
There's an important piece that really shows you the power of a generator. You want to be able to pause this function. To pause execution, you need the yield keyword.
function* fetchData() {
const data = yield fetchMovies();
}
When you execute the function, it returns an iterator that you can control with the next() method:
function* fetchMovies() {
const data = yield fetch();
}
const iter = fetchData();
iter.next(); // run the generator function up to the `yield` keyword.
Using the yield keyword tells the function to stop and wait. While it's paused, nothing else will execute until another next call happens.
The next() method returns an object with two properties, value and done. Value is the value being passed to the yield function, and done is a Boolean letting you know if the generator is finished executing. This object is also known as the IteratorResult interface (from the spec).
Here's a more complete example:
function* fetchUsers() {
yield fetch('https://jsonplaceholder.typicode.com/users')
.then(resp => resp.json())
.then(users => {
return users;
});
}
const usersIt = fetchUsers();
usersIt.next().value.then(resp => console.log(resp));
Refresher on Async/Await
An async function:
- Always returns a promise
- Requires the async keyword before the function declaration
- Uses a try/catch block for "rejected" promises
- Lets you skip putting .then(...) on async calls
Async functions can help improve the readability of asynchronous code. For a function to use async/await, it only needs to be declared as async. Here's an example of the fetchUsers function rewritten into a strictly async function (no generator):
async function fetchUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await response.json();
return users;
}
You can still make this function a generator and pause the function whenever you'd like:
async function* fetchUsers() {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
yield await response.json();
}
const it = fetchUsers();
it.next().then(({ value, done }) => {
console.log(value);
});
What you may notice here is that the next() method isn't returning the IteratorResult interface { value, done }. It is instead returning a promise. This is because async functions return promises. Calling next() advances function execution and returns any value passed to the next yield. Since this is an async function, that value will be a promise that needs to be resolved. In this case, the promise will resolve with the value of response.json(). That resolved value is still assigned to the iterator's { value, done } object, which I am destructuring in the code above.
Conclusion
Remember, when executing an async generator:
- Advancing an async generator function with next() will return a promise
- That promise will resolve with the IteratorResult interface of { value, done }
I hope this helps explain how to use async/await with a generator function. For more on generators, please check out my course Javascript Generators and Iterators, where I give a complete introduction with examples of generator functions and iterators.