JS Tip of the Day: Async Generators

Async Generators
Version: ES2018
Level: Advanced

Normally, when next() is called in an iterator, it returns a result object (an object with value and done properties). This is by design as part of the iterator protocol. Some iterators may instead return a promise from their next() that, when resolved, results in an iterator result object. In that case, the iterator would be an async iterator.

Generator functions that are defined as async will create async iterators (async generator objects). Instead of returning result objects, calling next() on an async generator iterator will return a promise. That promise resolves to a result object when a yield in the generator function is reached.

async function * drums (count, delay) {
    while (count-- > 0) {
        await new Promise(resolve => setTimeout(resolve, delay));
        yield `boom: ${count + 1}`; // resolves next() promise
    }
}

let asyncIter = drums(3, 500);
let result = asyncIter.next();

console.log(result instanceof Promise); // true
result.then(value => console.log(value));
// {value: "boom: 3", done: false}

To easily iterate through an async iterator, you can use a for await...of loop. This will automatically await the returned promises from the iterator before moving on to the next iteration.

async function * drums (count, delay) {
    while (count-- > 0) {
        await new Promise(resolve => setTimeout(resolve, delay));
        yield `boom: ${count + 1}`; // resolves next() promise
    }
}

async function danceToTheBeat () {
    for await (let beat of drums(3, 500)) {
        console.log(`Dancing to the ${beat}`);
    }
}

danceToTheBeat();
/* logs (over time):
Dancing to the boom: 3
Dancing to the boom: 2
Dancing to the boom: 1
*/

Where normal iterables are iterable thanks to and Symbol.iterator property, async iterables instead implement a Symbol.asyncIterator property. An object can implement both to be both an iterable and an async iterable.

let allKindsOfIterable = {
    * [Symbol.iterator] () {
        yield 'sync a';
        yield 'sync b';
        yield 'sync c';
    },
    async * [Symbol.asyncIterator] () {
        yield 'async x';
        yield 'async y';
        yield 'async z';
    }
}
for (let msg of allKindsOfIterable) {
    console.log(msg);
}
/* logs:
sync a
sync b
sync c
*/

for await (let msg of allKindsOfIterable) {
    console.log(msg);
}
/* logs:
async x
async y
async z
*/

Async iterators can be used for streams of data (note: these “streams” are different from the streams you see in Node). Instead of loading a large mass of data all at once, it can be loaded in individual chunks, each received in an async iteration of an async iterator. For an example of this, see the for await...of MDN link below.

More info:


More tips: JavaScript Tips of the Day