skip to Main Content

I know there are plenty of resources explaining for await...of, but I don’t think I’ll ever fully grasp the concept until I see an example that works the exact same way but with more basic syntax.

Thing is, if my understanding is correct, these 2 for loops should behave the exact same way:

for(const p of iterableWithPromises) {
  const q = await p;
  // ...
}

for await (const q of iterableWithPromises) {
  // ...
}

Is that it? Is this syntax only saving us from creating a new variable inside the loop to store the promise’s result?

2

Answers


  1. Chosen as BEST ANSWER

    No, the 2 loops are not exactly equivalent.

    First things first: This is, roughly speaking, what the equivalent of the traditional for ...of loop looks like with normal iterables (omiting corner cases like the usage of break, continue, exceptions and returns in the loop for brevity):

    // Given this iterable...
    const iterable = {
      [Symbol.iterator]() {
        console.log('[Symbol.iterator]() called');
        let i = 0;
        return {
          next() {
            console.log('next() called');
            const iteratorResult = { value: i, done: i > 3 };
            i++;
            return iteratorResult;
          },
        };
      },
    };
    
    // ...this for loop...
    for (const value of iterable) {
      console.log(`VALUE: ${value}`);
    }
    
    // ...is equivalent to this while loop:
    const iterator = iterable[Symbol.iterator]();
    let iteratorResult = iterator.next();
    while(!iteratorResult.done){
    const value = iteratorResult.value;
      console.log(`VALUE: ${value}`);
      iteratorResult = iterator.next();
    }
    
    // And this would be the output:
    //
    // [Symbol.iterator]() called
    // next() called
    // VALUE: 0
    // next() called
    // VALUE: 1
    // next() called
    // VALUE: 2
    // next() called
    // VALUE: 3
    // next() called
    

    Now, then, with for await...of and async iterables, the equivalent would be like this:

    const makePromise = (name, seconds, value) => {
      console.log(`Creating a promise named ${name} that will resolve in ${seconds} seconds with a value of ${JSON.stringify(value)}...`);
      return new Promise((resolve) => {
        console.log(`Promise ${name} created`);
        setTimeout(() => {
          console.log(`Resolving promise ${name}...`);
          resolve(value);
          console.log(`Promise ${name} resolved`);
        }, seconds*1000)
      });
    }
    
    // Given this iterable...
    const iterable = {
      [Symbol.asyncIterator]() {
        console.log('[Symbol.asyncIterator]() called');
        let i = 0;
        return {
          next() {
            console.log('next() called');
            const iteratorResult = makePromise(`promise-${i}`, i, { value: i, done: i > 3 });
            i++;
            return iteratorResult;
          },
        };
      },
    };
    
    // ...this for loop...
    for await (const value of iterable) {
      console.log(`VALUE: ${value}`);
    }
    
    // ...is equivalent to this while loop:
    const iterator = iterable[Symbol.asyncIterator]();
    let iteratorResult = await iterator.next();
    while(!iteratorResult.done){
      const value = iteratorResult.value;
      console.log(`VALUE: ${value}`);
      iteratorResult = await iterator.next();
    }
    
    // And this would be the output:
    //
    // [Symbol.asyncIterator]() called
    // next() called
    // Creating a promise named promise-0 that will resolve in 0 seconds with a value of {"value":0,"done":false}...
    // Promise promise-0 created
    // (0 seconds later...)
    // Resolving promise promise-0...
    // Promise promise-0 resolved
    // VALUE: 0
    // next() called
    // Creating a promise named promise-1 that will resolve in 1 seconds with a value of {"value":1,"done":false}...
    // Promise promise-1 created
    // (1 second later...)
    // Resolving promise promise-1...
    // Promise promise-1 resolved
    // VALUE: 1
    // next() called
    // Creating a promise named promise-2 that will resolve in 2 seconds with a value of {"value":2,"done":false}...
    // Promise promise-2 created
    // (2 seconds later...)
    // Resolving promise promise-2...
    // Promise promise-2 resolved
    // VALUE: 2
    // next() called
    // Creating a promise named promise-3 that will resolve in 3 seconds with a value of {"value":3,"done":false}...
    // Promise promise-3 created
    // (3 seconds later...)
    // Resolving promise promise-3...
    // Promise promise-3 resolved
    // VALUE: 3
    // next() called
    // Creating a promise named promise-4 that will resolve in 4 seconds with a value of {"value":4,"done":true}...
    // Promise promise-4 created
    // (4 seconds later...)
    // Resolving promise promise-4...
    // Promise promise-4 resolved
    

    Now let's say my initial wrong assumption about how this kind of for loop works was right. Then we should be able to replace the async iterable with a normal iterable, like so:

    const iterable = {
      [Symbol.iterator]() {
        console.log('[Symbol.iterator]() called');
        let i = 0;
        return {
          next() {
            console.log('next() called');
            const iteratorResult = {
              value: makePromise(`promise-${i}`, i, i),
              done: i > 3
            };
            i++;
            return iteratorResult;
          },
        };
      },
    };
    

    If you run any example with this last iterable you will notice no difference in the results. Not even in the times between one output and the next. But there is something you should notice: The done property of the object returned by next() is included inside the promise when using for await...of. This is relevant in cases where deciding whether the for loop should stop iterating depends on the result of the promise.

    For instance, let's say you have a REST api that has in one of the fields of the response json object a url to keep fetching the next results: You could still technically implement this with a normal iterator, but with a few caveats:

    • First, the first time next() is evaluated you are forced to make sure done is evaluated to true regardless of whether the REST api has actually any data or not to make sure at least the first request is made, otherwise no request would be made to begin with and you wouldn't have any way to tell if there is any data at all (if it evaluated to false, the request would still be done but the loop would end without any chance to do anything with the data and you won't be able to do anything about it).
    • Second, you will assume that the developer will always dutifully await each result in each iteration of the loop before reaching the next iteration. So, if a developer had the brilliant idea of not awaiting to make the requests run in parallel, then it would run an infinity loop that would only stop once the first promise finally resolves and updates the value of the done property for the first time. You can prevent this by fetching the data in the server implementing async iterators as I did in the examples (Notice how in the for await...of example I use [Symbol.asyncIterator] instead of [Symbol.iterator]) to force the developer to use for await...of and prevent these problems.

  2. Of course. You are correct that the two loops share a similar purpose: they both iterate over an iterable containing promises and allow you to handle their resolved values. However, for await...of provides cleaner and more concise syntax specifically designed to handle asynchronous iterables, while your first example is a more verbose way of achieving the same result with a regular for loop.

    Why use for await...of?

    The for await...of loop is specifically useful when dealing with asynchronous iterables, which provide values over time, such as streams or generators that yield promises.

    Example: Using an asynchronous generator

    async function* asyncGenerator() {
      yield Promise.resolve(1);
      yield Promise.resolve(2);
      yield Promise.resolve(3);
    }
    
    (async () => {
      for await (const value of asyncGenerator()) {
        console.log(value); // Logs 1, then 2, then 3
      }
    })();
    

    This cannot be easily replicated with a for...of loop because for...of does not inherently understand asynchronous iterables.

    And if you want to get the hood , go for the sepc.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search