7

I am trying to understand javascript's Symbol.asyncIterator and for await of. I wrote some simple code and it throws an error saying:

    TypeError: undefined is not a function

on the line which tries to use for await (let x of a).

I could not understand the reason for it.

let a = {}


function test() {
        for(let i=0; i < 10; i++) {
                if(i > 5) {
                        return Promise.resolve(`Greater than 5: (${i})`)
                }else {
                        return Promise.resolve(`Less than 5: (${i})`)
                }
        }
}

a[Symbol.asyncIterator] = test;


async function main() {
        for await (let x of a) { // LINE THAT THROWS AN ERROR
                console.log(x)
        }
}


main()
        .then(r => console.log(r))
        .catch(err => console.log(err))

I create an empty object a and insert a key Symbol.asyncIterator on the same object and assign it a function named test that returns a Promise. Then I use for await of loop to iterate over all the values that the function would return.

What am I doing incorrectly?

PS: I am on the Node version 10.13.0 and on the latest version of Chrome

Suhail Gupta
  • 22,386
  • 64
  • 200
  • 328

3 Answers3

7

To be a valid asyncIterator, your test function must return an object with a next method that returns a promise of a result object with value and done properties. (Technically, value is optional if its value would be undefined and done is optional if its value would be false, but...)

You can do that in a few ways:

  1. Completely manually (awkward, particularly if you want the right prototype)
  2. Half-manually (slightly less awkward, but still awkward to get the right prototype)
  3. Using an async generator function (simplest)

You can do it completely manually (this doesn't try to get the right prototype):

function test() {
    let i = -1;
    return {
        next() {
            ++i;
            if (i >= 10) {
                return Promise.resolve({
                    value: undefined,
                    done: true
                });
            }
            return Promise.resolve({
                value: i > 5 ? `Greater than 5: (${i})` : `Less than 5: (${i})`,
                done: false
            });
        }
    };
}

let a = {
    [Symbol.asyncIterator]: test
};

async function main() {
    for await (let x of a) {
        console.log(x)
    }
}

main()
    .then(r => console.log(r))
    .catch(err => console.log(err))

You can do it half-manually writing a function that returns an object with an async next method (still doesn't try to get the right prototype):

function test() {
    let i = -1;
    return {
        async next() {
            ++i;
            if (i >= 10) {
                return {
                    value: undefined,
                    done: true
                };
            }
            return {
                value: i > 5 ? `Greater than 5: (${i})` : `Less than 5: (${i})`,
                done: false
            };
        }
    };
}

let a = {
    [Symbol.asyncIterator]: test
};

async function main() {
    for await (let x of a) {
        console.log(x)
    }
}

main()
    .then(r => console.log(r))
    .catch(err => console.log(err))

Or you can just use an async generator function (easiest, and automatically gets the right prototype):

async function* test() {
    for (let i = 0; i < 10; ++i) {
        yield i > 5 ? `Greater than 5: (${i})` : `Less than 5: (${i})`;
    }
}

let a = {
    [Symbol.asyncIterator]: test
};

async function main() {
    for await (let x of a) {
        console.log(x)
    }
}

main()
    .then(r => console.log(r))
    .catch(err => console.log(err))

About prototypes: All async iterators you get from the JavaScript runtime itself inherit from a prototype that provides the very basic feature of ensuring the iterator is also iterable (by having Symbol.iterator be a function returning this). There's no publicly-available identifer or property for that prototype, you have to jump through hoops to get it:

const asyncIteratorPrototype =
    Object.getPrototypeOf(
        Object.getPrototypeOf(
            async function*(){}.prototype
        )
    );

Then you'd use that as the prototype of the object with the next method that you're returning:

return Object.assign(Object.create(asyncIteratorPrototype), {
    next() {
        // ...
    }
});
T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • For the last part ... wouldn't `[Symbol.asyncIterator]() { return this; }` be enough ? – Jonas Wilms Apr 05 '19 at 08:52
  • @JonasWilms - Yes...unless your project enhances async iterators by adding features to the prototype. :-) Since TC39 made it such a pain to get the prototype, I don't think anyone will ever actually expect enhancing the prototype to work with anything other than native async iterators and ones they've written themselves ensuring the prototype. So in practice, almost all the time, yes. – T.J. Crowder Apr 05 '19 at 08:56
  • 1
    When using with `yield`, does it implicitly return `value` and `done`? – Suhail Gupta Apr 05 '19 at 09:43
  • 2
    @SuhailGupta - Yes, the generator / async generator itself provides the wrapper result object, which is part of why generators are so handy for creating iterators. With `yield`, the result object will have the yielded value and `done: false`. With `return` (in a generator function), the result object will have the yielded value and `done: true`. (Similarly: `for-await-of` and `for-of` consume the result object, using its `value` and `next` under the covers.) – T.J. Crowder Apr 05 '19 at 09:48
2

The test function must not return a promise, but an Iterator (an object with a next() ) method, that method then has to return a Promise (which makes it an async iterator) and that Promise has to resolve to an object containing a value and a done key:

function test() {
   return {
     next() {
       return Promise.resolve({ value: "test", done: false });
     }
   };
}

Now while that works, it is not that useful yet. You could however create the same behaviour with an async generator function:

  async function* test() {
    await Promise.resolve();
    yield "test";
  }

Or in your case:

async function* test() {
  for(let i = 0; i < 10; i++) {
    if(i > 5) {
      await Promise.resolve();
      yield `Greater than 5: (${i})`;
    }else {
      await Promise.resolve();
      yield `Less than 5: (${i})`;
    }
  }
}
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
0

You should make test an async generator function instead, and yield instead of return:

let a = {}


async function* test() {
  for(let i=0; i < 10; i++) {
    if(i > 5) {
      yield Promise.resolve(`Greater than 5: (${i})`)
    }else {
      yield Promise.resolve(`Less than 5: (${i})`)
    }
  }
}

a[Symbol.asyncIterator] = test;


async function main() {
  for await (let x of a) {
    console.log(x)
  }
}


main()
  .then(r => console.log(r))
  .catch(err => console.log(err))

It looks like the test function needs to be async so that the x in the for await gets unwrapped, even though test doesn't await anywhere, otherwise the x will be a Promise that resolves to the value, not the value itself.

yielding Promise.resolve inside an async generator is odd, though - unless you want the result to be a Promise (which would require an extra await inside the for await loop), it'll make more sense to await inside the async generator, and then yield the result.

const delay = ms => new Promise(res => setTimeout(res, ms));
let a = {}


async function* test() {
  for(let i=0; i < 10; i++) {
    await delay(500);
    if(i > 5) {
      yield `Greater than 5: (${i})`;
    }else {
      yield `Less than 5: (${i})`;
    }
  }
}

a[Symbol.asyncIterator] = test;


async function main() {
  for await (let x of a) {
    console.log(x)
  }
}


main()
  .then(r => console.log(r))
  .catch(err => console.log(err))

If you didn't make test a generator, test would have to return an iterator (an object with a value property and a next function).

CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Thats not an async iterator though. – Jonas Wilms Apr 05 '19 at 08:31
  • 1
    You've written an async generator. There's no reason to use `Promise.resolve` if you're going to use an async generator to do this, yield the values directly; it wraps them for you. But I thought the point from the OP was that they wanted to understand these under the covers. – T.J. Crowder Apr 05 '19 at 08:37
  • @T.J.Crowder I thought the `Promise.resolve` was there just to imitate something asynchronous being done - in non-example code, there'd probably be an `await` in there, so the `async` function would make more sense, right? – CertainPerformance Apr 05 '19 at 08:39
  • 1
    @CertainPerformance - There's no reason to `yield await somePromise` in an async generator, just like there's no reason to `return await somePromise` in an `async` function; in both cases, the value is automatically awaited (effectively). Until a recent spec change, adding the `await` just delayed things by an extra async tick. (With the latest spec change, done just after ES2019, as long as what you `await` is a *native* promise, that extra tick gets optimized away.) – T.J. Crowder Apr 05 '19 at 08:50
  • @t.j.crowder yet the [proposal](https://github.com/tc39/proposal-async-iteration/blob/master/README.md) does use `yield await` itself. – Jonas Wilms Apr 05 '19 at 09:06
  • 1
    @JonasWilms - Well, nobody's perfect. :-) It wouldn't be surprising if the text of the proposal got slightly out of date as things progressed. For details on the changes to make `yield await promise` and `return await promise` perform better: https://v8.dev/blog/fast-async Also see Mathias Bynens' replies to my tweet [here](https://twitter.com/tjcrowder/status/1109894986689069056). :-) – T.J. Crowder Apr 05 '19 at 09:09