Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using javascript's Symbol.asyncIterator with for await of loop

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

like image 469
Suhail Gupta Avatar asked Apr 05 '19 08:04

Suhail Gupta


2 Answers

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() {
        // ...
    }
});
like image 83
T.J. Crowder Avatar answered Oct 19 '22 04:10

T.J. Crowder


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})`;
    }
  }
}
like image 22
Jonas Wilms Avatar answered Oct 19 '22 02:10

Jonas Wilms