Search code examples
javascriptnode.jshyperledger-fabric

await in for-of loop vs fetching the iterable before for-of


See the two code blocks below:

for await (const {key, value} of ctx.stub.getStateByRange(startKey, endKey)) {}

and

let allCarsRangeIterator = await ctx.stub.getStateByRange(startKey, endKey);
for (const {key, value} of allCarsRangeIterator ) {}

When I use the first one, everything works, but when I use the second one, I get an error saying allCarsRangeIterator is not iterable.

Why are these two not equivalent ?


Solution

  • I found this to be a very interesting question and dug around for a bit. Let's see if I can answer this properly:

    First, we need to be clear about iterators and generators, so I encourage everyone to go through the linked documentation.

    The following seems to be a generator:

    ctx.stub.getStateByRange(startKey, endKey)
    

    As per the docs, a generator is a special type of iterator that can be iterated over only once. On top of that, it seems to be a generator that does some async computation. Let's mock this with a simpler function:

    async function* foo() {
      yield 1;
      yield new Promise((resolve) => setTimeout(resolve, 2000, 2));
      yield 3;
    }
    

    As per the docs, in order for an object to be iterable, it needs to implement the Symbol.asyncIterator (async) or Symbol.iterator (sync) protocol. Let's check this:

    async function* foo() {
      yield 1;
      yield new Promise((resolve) => setTimeout(resolve, 2000, 2));
      yield 3;
    }
    
    (async function () {
      const obj1 = foo();
      console.log(typeof obj1[Symbol.iterator] === "function"); // false
      console.log(typeof obj1[Symbol.asyncIterator] === "function"); // true
    })();
    

    Now let's see the output if we use await.

    async function* foo() {
      yield 1;
      yield new Promise((resolve) => setTimeout(resolve, 2000, 2));
      yield 3;
    }
    
    (async function () {
      const obj1 = await foo();
      console.log(typeof obj1[Symbol.iterator] === "function"); // false
      console.log(typeof obj1[Symbol.asyncIterator] === "function"); // true
    })();
    

    It's still the same. This conforms to the error you got: allCarsRangeIterator is not iterable, because obj1 does not implement the Symbol.iterator protocol but rather the Symbol.asyncIterator protocol.

    So, when you are trying to use for (const num of await foo()), it is a synchronous code, so it is going to look for Symbol.iterator protocol implementation. But since your function is an async generator, it only has Symbol.asyncIterator protocol implemented.

    The only way to iterate over an async generator is therefore:

    for await (const num of foo()) {
        console.log(num);
    }
    

    You will also see that this is valid (although it looks weird)

    for await (const num of await foo()) {
        console.log(num);
    }
    

    Since (const num of await foo()) still has the Symbol.asyncIterator protocol implemented.


    In fact, in VSCode, you will see a warning whenever you type for (const num of await foo()):

    Type 'AsyncGenerator<any, void, unknown>' must have a '[Symbol.iterator]()' method that returns an iterator. ts(2488)

    and const obj1 = await foo() shows the warning:

    'await' has no effect on the type of this expression. ts(80007)


    Additional resources:

    1. for...await of
    2. iterable and iterator protocols