I’m trying to get the following async generator to work:
class MyIterator { constructor(m) { this.collection = m; } async *[Symbol.iterator]() { for (let item of this.collection) { const resultItem = await Promise.resolve(item) console.log("item: ", resultItem) yield resultItem } } } (async () => { const iterator = new MyIterator([1,2,3]) let times = 0 for await (let thing of iterator) { console.log("thing: ", thing) // this is here to avoid an infinite loop times++ if (times > 1000) break } })()
But it ends up in an infinite loop, and thing
is always undefined.
item: 1 thing: undefined item: 2 thing: undefined item: 3 thing: undefined (x999)
I’ve tried a similar code, but this time without the Promise/async
behaviour, and it seems to work just fine.
class MyIterator { constructor(m) { this.collection = m; } *[Symbol.iterator]() { for (let item of this.collection) { console.log("item: ", item) yield item } } } const iterator = new MyIterator([1,2,3]) for (let thing of iterator) { console.log("thing: ", thing) }
item: 1 thing: 1 item: 2 thing: 2 item: 3 thing: 3
Advertisement
Answer
The for await..of
construct will attempt to iterate over an async iterator.
An async iterator is defined using the @@asyncIterator
well-known symbol:
class MyIterator { constructor(m) { this.collection = m; } async *[Symbol.asyncIterator]() { //<-- this is async for (let item of this.collection) { const resultItem = await Promise.resolve(item) //console.log("item: ", resultItem) yield resultItem } } } (async () => { const iterator = new MyIterator([1,2,3]) let times = 0 for await (let thing of iterator) { //no infinite loop console.log("thing: ", thing) } })()
for await..of
can also consume plain iterables that produce promises:
const promiseArray = [Promise.resolve("a"), Promise.resolve("b"), Promise.resolve("c")]; (async function() { for await(const item of promiseArray) { console.log(item); } })()
Attempting to make a regular iterator that is an async method/function does not work.
If you want to keep your @@iterator
defined method your the best choice is to make it produce promises instead:
class MyIterator { constructor(m) { this.collection = m; } *[Symbol.iterator]() { // not async for (let item of this.collection) { yield Promise.resolve(item); //produce a promise } } } (async () => { const iterator = new MyIterator([1,2,3]) let times = 0 for await (let thing of iterator) { console.log("thing: ", thing) } })()
Although, that’s might be a bad practice if any of the promises rejects:
const wait = (ms, val) => new Promise(res => setTimeout(res, ms, val)); const fail = (ms, val) => new Promise((_, rej) => setTimeout(rej, ms, val)); const arr = [ wait(100, 1), wait(150, 2), fail(0, "boom"), wait(200, 3) ]; (async function(){ try { for await (const item of arr) { console.log(item); } } catch (e) { console.error(e); } })() /* result in the browser console: Uncaught (in promise) boom 1 2 boom */
However, be aware that there is a difference in semantics between these:
- A regular iterator produces an IteratorResult – an object with
value
anddone
properties.
const syncIterable = { [Symbol.iterator]() { return { next() { return {value: 1, done: true} } } } } const syncIterator = syncIterable[Symbol.iterator](); console.log("sync IteratorResult", syncIterator.next());
- An async generator produces a promise for an IteratorResult
const asyncIterable = { [Symbol.asyncIterator]() { return { next() { return Promise.resolve({value: 2, done: true}); } } } } const asyncIterator = asyncIterable[Symbol.asyncIterator](); asyncIterator.next().then(result => console.log("async IteratorResult", result));
- Finally, an iterator that produces promises will have an IteratorResult where
value
is a promise:
const promiseSyncIterable = { [Symbol.iterator]() { return { next() { return {value: Promise.resolve(3), done: true} } } } } const promiseSyncIterator = promiseSyncIterable[Symbol.iterator](); const syncPromiseIteratorResult = promiseSyncIterator.next(); console.log("sync IteratorResult with promise", syncPromiseIteratorResult); syncPromiseIteratorResult.value .then(value => console.log("value of sync IteratorResult with promise", value));
Side-note on nomenclature: MyIterator
is not an iterator. An iterator is an object with a next()
method which produces an IteratorResult. An object that you can iterate over has an @@iterator
(or @@asyncIterable
) method and it is called iterable (or async iterable respectively).