1

This code works as expected:

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function getAsyncData() {
    await sleep(1000);  // simulate database/network delay...
    return [1, 2, 3, 4, 5];  // ...then return some data
}

const asyncIterable = (async function* filterAsyncData() {
    const items = await getAsyncData();

    for (const item of items) {
        yield item;
    }
})();

const asyncIterable2 = {
    [Symbol.asyncIterator]() {
        return {
            values: null,
            idx: 0,
            async next() {
                if (this.values === null) {
                    this.values = await getAsyncData();
                }

                if (this.idx < this.values.length) {
                    this.idx = this.idx + 1;
                    return Promise.resolve({ value: this.values[this.idx - 1], done: false });
                }

                return Promise.resolve({ done: true });
            }
        };
    }
};

async function main() {
    for await (const filteredItem of asyncIterable) {
        console.log(filteredItem);
    }
}

main()

It does not mather if I use asyncIterable or asyncIterable2 in the main function, I always get the same result. What is the best practice to define my iterable? Are there any guidelines about which option is preferred? Why?

enanone
  • 923
  • 11
  • 25
  • 1
    In this particular case, you actually should be doing `for (const filteredItem of await getAsyncData()) {` instead of using async iterators/iterables at all. – Bergi Jul 14 '20 at 16:35
  • You are completely right! But I do need the iterator because I am mocking the return value of an async generator function in a test :) – enanone Jul 14 '20 at 16:37

1 Answers1

5

It's the same as for synchronous iterators: generator functions are much easier to write, and easier to get correct, than implementing the iterator object manually. Do this only if you need some non-standard behaviour that cannot be achieved otherwise. With asynchronous generator functions specifically, you even get the proper queueing of next calls for free, which is a real headache to get right (your asyncIterable2 fails this1).

The most common implementation of iterables is to make the Symbol.asyncIterator method an async generator method:

const asyncIterable = {
    async *[Symbol.asyncIterator]() {
         yield* await getAsyncData();
    },
};

1: const it = asyncIterable2[Symbol.asyncIterator](); it.next(); it.next() - without any awaits in between - will call getAsyncData twice, because this.values == null in both calls

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • What is queuing of `next` calls? And why am I doing it wrong in `asyncIterable2`? EDIT: Now I have seen the edit. Thank you for the amazing answer! I wish I could give you more than one upvote :D – enanone Jul 14 '20 at 16:48