1

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
André Herculano
  • 1,258
  • 20
  • 33
  • Try using `then()` instead of await. That way it will only yield the next iteration when the first one is done. – zergski Mar 08 '21 at 17:04
  • 1
    @zergski no, that's incorrect. – Pointy Mar 08 '21 at 17:05
  • 3
    `Symbol.iterator` -> `Symbol.asyncIterator`? You *do* want to define an async iterator, right? You've marked it as a regular one. – VLAZ Mar 08 '21 at 17:05
  • That was it @VLAZ , I wasn't aware of `Symbol.asyncIterator`. Once I changed from `.iterator` to `.asyncIterator` it all worked. And thank you for pointing out about the nomenclature as well. `MyIterator` implements the [iterable protocol](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol), not the iterator one. – André Herculano Mar 08 '21 at 17:54

2 Answers2

1

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
*/

Screenshot of the browser console from running the above snippet. Results are identical to the comment left in at the end of the snippet.

However, be aware that there is a difference in semantics between these:

  • A regular iterator produces an IteratorResult - an object with value and done 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).

VLAZ
  • 26,331
  • 9
  • 49
  • 67
0

As @VLAZ pointed out in a comment to my question, I was using Symbol.iterator instead of Symbol.asyncIterator. The following implementation works as expected:

class MyIterator {
  constructor(m) {
    this.collection = m;
  }

   async *[Symbol.asyncIterator]() {
      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])
  for await (let thing of iterator) {
    console.log("thing: ", thing)
  }
})()
André Herculano
  • 1,258
  • 20
  • 33