8

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

1
  • You need to use generators and yield instead of return. should work then Commented Apr 5, 2019 at 8:39

3 Answers 3

8

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() {
        // ...
    }
});
Sign up to request clarification or add additional context in comments.

4 Comments

For the last part ... wouldn't [Symbol.asyncIterator]() { return this; } be enough ?
@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.
When using with yield, does it implicitly return value and done?
@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.)
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})`;
    }
  }
}

3 Comments

Could you explain the purpose of await Promise.resolve();
@suhail await halts the iterator until Promise.resolve() or any other asynchronous action wrapped in a promise, is done.
@SuhailGupta - E.g., in the above it just demonstrates using the power of async/await in the async generator.
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).

6 Comments

Thats not an async iterator though.
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 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 - 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.)
@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: v8.dev/blog/fast-async Also see Mathias Bynens' replies to my tweet here. :-)
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.