for-await-of and synchronous iterables

[2017-12-03] dev, javascript, esnext, es proposal
(Ad, please don’t block)

This blog post describes how for-await-of handles synchronous iterables. for-await-of is a core construct of asynchronous iteration. You can read up on it in the blog post “ES proposal: asynchronous iteration”.

Note: you can run the examples in Node.js 9.2+ via:

node --harmony-async-iteration

Refresher: asynchronous iterables  

Asynchronous iterables return asynchronous iterators, whose method next() returns Promises for {value, done} objects:

// Asynchronous generator
async function* asyncGen() {
    yield 'a';
    yield 'b';
}
const iter = asyncGen()[Symbol.asyncIterator]();

iter.next().then(x => console.log(x));
    // { value: 'a', done: false }
iter.next().then(x => console.log(x));
    // { value: 'b', done: false }
iter.next().then(x => console.log(x));
    // { value: undefined, done: true }

Synchronous iterables and for-await-of  

Synchronous iterables return synchronous iterators, whose method next() returns {value, done} objects. for-await-of handles synchronous iterables by converting them to asynchronous iterables. Each iterated value is converted to a Promise (or left unchanged if it already is a Promise) via Promise.resolve(). That is, for-await-of works for iterables over Promises and over normal values. The conversion looks like this:

const nextResult = Promise.resolve(valueOrPromise)
    .then(x => ({ value: x, done: false }));

Two more ways of looking at the conversion are:

  • Iterable<Promise<T>> becomes AsyncIterable<T>

  • The following object

    { value: Promise.resolve(123), done: false }
    

    is converted to

    Promise.resolve({ value: 123, done: false })
    

Therefore, the following two statements are roughly similar.

for (const x of await Promise.all(syncIterableOverPromises));
for await (const x of syncIterableOverPromises);

The second statement is faster, because Promise.all() only creates the Promise for the Array after all Promises in syncIterableOverPromises are fulfilled. And for-of has to await that Promise. In contrast, for-await-of starts processing as soon as the first Promise is fulfilled.

for-await-of in action  

Iterating over a sync iterable over Promises:

async function main() {
    const syncIterable = [
        Promise.resolve('a'),
        Promise.resolve('b'),
    ];
    for await (const x of syncIterable) {
        console.log(x);
    }
}
main();

// Output:
// a
// b

Iterating over a sync iterable over normal values:

async function main() {
    for await (const x of ['c', 'd']) {
        console.log(x);
    }
}
main();

// Output:
// c
// d

Further reading