The TC39 async iterators proposal that brought for/await/of
to JavaScript also introduced the concept of an async generator function. Now, JavaScript has 6 distinct types of functions:
- Normal functions
function() {}
- Arrow functions
() => {}
- Async functions
async function() {}
- Async arrow functions
async () => {}
- Generator functions
function*() {}
- Async generator functions
async function*() {}
Async generator functions are special because you can use both await
and yield
in an async generator function. Async generator functions
differ from async functions and generator functions in that they don't
return a promise or an iterator, but rather an async iterator. You can think of an async iterator as an iterator whose next()
function always returns a promise.
Your First Async Generator Function
Async generator functions behave similarly to generator functions: the
generator function returns an object that has a next()
function, and
calling next()
executes the generator function until the next yield
.
The difference is that an
async iterator's next()
function returns a promise.
Below is a "Hello, World" example with async generator functions. Note that the below script won't work on Node.js versions before 10.x.
'use strict';
async function* run() {
// Sleep for 100ms, see: https://masteringjs.io/tutorials/fundamentals/sleep
await new Promise(resolve => setTimeout(resolve, 100));
yield 'Hello';
console.log('World');
}
// `run()` returns an async iterator.
const asyncIterator = run();
// The function doesn't start running until you call `next()`
asyncIterator.next().
then(obj => console.log(obj.value)). // Prints "Hello"
then(() => asyncIterator.next()); // Prints "World"
The cleanest way to loop through an entire async generator function
is using a for/await/of
loop.
'use strict';
async function* run() {
await new Promise(resolve => setTimeout(resolve, 100));
yield 'Hello';
console.log('World');
}
const asyncIterator = run();
// Prints "Hello\nWorld"
(async () => {
for await (const val of asyncIterator) {
console.log(val); // Prints "Hello"
}
})();
A Practical Use Case
You might be thinking "why does JavaScript need async generator functions when it already has async functions and generator functions?" One use case is the classic progress bar problem that Ryan Dahl originally wrote Node.js to solve.
Suppose you want to loop through all documents in a Mongoose cursor and report progress via websocket or to the command line.
'use strict';
const mongoose = require('mongoose');
async function* run() {
await mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true });
await mongoose.connection.dropDatabase();
const Model = mongoose.model('Test', mongoose.Schema({ name: String }));
for (let i = 0; i < 5; ++i) {
await Model.create({ name: `doc ${i}` });
}
// Suppose you have a lot of documents and you want to report when you process
// each one. You can `yield` after processing each individual doc.
const total = 5;
const cursor = Model.find().cursor();
let processed = 0;
for await (const doc of cursor) {
// You can think of `yield` as reporting "I'm done with one unit of work"
yield { processed: ++processed, total };
}
}
(async () => {
for await (const val of run()) {
// Prints "1 / 5", "2 / 5", "3 / 5", etc.
console.log(`${val.processed} / ${val.total}`);
}
})();
Async generator functions make it easy for your async function to report
its progress in a framework-free way. No need to explicitly create a websocket
or log to the console - you can handle that separately if you assume
your business logic uses yield
for progress reporting.
With Observables
Async iterators are great, but there's another concurrency primitive that async generator functions align well with: RxJS observables.
'use strict';
const { Observable } = require('rxjs');
const mongoose = require('mongoose');
async function* run() {
// Same as before
}
// Create an observable that emits each value the async generator yields
// to subscribers.
const observable = Observable.create(async (observer) => {
for await (const val of run()) {
observer.next(val);
}
});
// Prints "1 / 5", "2 / 5", "3 / 5", etc.
observable.subscribe(val => console.log(`${val.processed} / ${val.total}`));
There are two key differences between using an RxJS observable versus
an async iterator. First, in the above example the code that logs to
the console in subscribe()
is reactive rather than imperative.
In other words, the subscribe()
handler has no way of affecting the
code in the async function body, it merely reacts to events. When using
a for/await/of
loop, you can, for instance, add a 1 second pause
before resuming the async generator function.
(async () => {
for await (const val of run()) {
// Prints "1 / 5", "2 / 5", "3 / 5", etc.
console.log(`${val.processed} / ${val.total}`);
// This adds a 1 second delay to every `yield` statement.
await new Promise(resolve => setTimeout(resolve, 1000));
}
})();
The second is that, since RxJS observables are cold by default, a new subscribe()
call re-executes the function.
// Prints "1 / 5", "2 / 5", "3 / 5", etc.
observable.subscribe(val => console.log(`${val.processed} / ${val.total}`));
// Kicks off a separate instance of `run()`
observable.subscribe(val => console.log(`${val.processed} / ${val.total}`));
Moving On
Async generator functions may seem niche and confusing at first, but
they provide what may become JavaScript's native solution to the progress
bar problem. Using yield
to report an async function's progress is
an enticing idea because it allows you to decouple your business logic
from your progress reporting framework. Give async generators a shot
next time you need to implement a progress bar.
Looking to become fluent in async/await? My new ebook, Mastering Async/Await, is designed to give you an integrated understanding of async/await fundamentals and how async/await fits in the JavaScript ecosystem in a few hours. Get your copy!