Async Generator Functions in JavaScript


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.

; async function* run() { await new Promise(resolve => setTimeout(resolve, 100)); yield 'Hello'; console.log('World');
} const asyncIterator = run(); asyncIterator.next(). then(obj => console.log(obj.value)). then(() => asyncIterator.next()); 

The cleanest way to loop through an entire async generator function is using a for/await/of loop.

; async function* run() { await new Promise(resolve => setTimeout(resolve, 100)); yield 'Hello'; console.log('World');
} const asyncIterator = run(); (async () => { for await (const val of asyncIterator) { console.log(val); }
})();

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.

; 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}` }); } const total = 5; const cursor = Model.find().cursor(); let processed = 0; for await (const doc of cursor) { yield { processed: ++processed, total }; }
} (async () => { for await (const val of run()) { 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.

; const { Observable } = require('rxjs');
const mongoose = require('mongoose'); async function* run() { } const observable = Observable.create(async (observer) => { for await (const val of run()) { observer.next(val); }
}); 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()) { console.log(`${val.processed} / ${val.total}`); 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.


observable.subscribe(val => console.log(`${val.processed} / ${val.total}`)); 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!

Found a typo or error? Open up a pull request! This post is available as markdown on Github