What's the control flow of "for await of"?

Viewed 1468

(English is not my native so please pardon my English in advance if you find any)

At this point I'm pretty comfortable with Promises and Async await function, I also know how to use Promise.all (which just waits for all the promises inside to resolve first and then extract the value from all the promises and then return an array using the .then's function with those values.)

Now I'm wondering how does for await of work under the hood.

I've just written this code:

async function f() {

  let p1 = new Promise(function (r) {
    setTimeout(r, 3000, 1)
  })

  let p2 = new Promise(function (r) {
    setTimeout(r, 2000, 2)
  })

  let p3 = new Promise(function (r) {
    setTimeout(r, 1000, 3)
  })

  let arrayOfPromises = [p1, p2, p3];

  for await (let p of arrayOfPromises) {
    let data = await p;
    console.log(data)
  }
}

f();

Now my question is what happens when it hits the first iteration, it will hit an await keyword, and await immediately returns a pending promise, so is the code below technically evaluated to a pending promise in each iteration?

  {
    let data = await p;
    console.log(data)
  }

So i'm confused whats going on really, for the first iteration, a setTimeout will be registered for 3 seconds, 2 for the second and 1 for the one. Since we have no sync code, all the callbacks will be run one by one, p3 will be resolved first, then p2 and then finally p1!

Now intuitively I would think this code "console.log(data)" will be put into the micro task queue once p1, p2, p3 are resolved and since our p3 was resolved first, we should get 3, 2, 1 but we're getting 1, 2, 3, so whats lacking in my understanding?

(Obviously code is not put into the microtask queue, its the functions that do so maybe its doing something like .next() like a generator function does but I think that doesnt matter here)

It seems like with for await of, the first promise will be logged first no matter what how quickly or lately its resolved compared to other promises in the iteration, so whats really going on?

2 Answers

Here's a code that will work as you intended:

async function printWhenResolved(p) { 
  const data = await p; console.log(data)
}

function createIteratorsAndBindPrinting() {   

  let p1 = new Promise(function (r) {
    setTimeout(r, 3000, 1)
  })

  let p2 = new Promise(function (r) {
    setTimeout(r, 2000, 2)
  })

  let p3 = new Promise(function (r) {
    setTimeout(r, 1000, 3)
  })

  let arrayOfPromises = [p1, p2, p3];

  for (let p of arrayOfPromises) {
    printWhenResolved(p)
  } 
  console.log('end of createIteratorsAndBindPrinting')
}
createIteratorsAndBindPrinting()

Now, semantically, I think the handling appears clear. On "for await (... of ...)", an iterable is iterated, at each point a decision is made based on whether the current value of the iteration provides its value synchronously or asynchronously. If it provides its value synchronously, we immediately execute the code block in the "for". If it provides its value asynchronously, we wait till the value is provided and then call the block in the "for".

So we await each iterable in turn - and only then leave the for-block.

In the code I provided, we instead call an asynchronous function on each promise, which, when it has resolves, will log the value to the console. This is also why your let data = await p is redundant (since your await is also in the for-construct).

In this way, async functions are a way to organize reactive code (i.e.code where a procedure is called upon and with e.g. the resolution of a promise) without either callback-hell, or then-chaining. This arguably allows for more modularity, expressivity and testability. See also https://stackoverflow.com/a/54497100/16005185

To see the difference to what specifically your code does (without the redunancy), compare the above code with this:

async function createIteratorsAwaitAndPrint() {   

  let p1 = new Promise(function (r) {
    setTimeout(r, 3000, 1)
  })

  let p2 = new Promise(function (r) {
    setTimeout(r, 2000, 2)
  })

  let p3 = new Promise(function (r) {
    setTimeout(r, 1000, 3)
  })

  let arrayOfPromises = [p1, p2, p3];

  for await (let p of arrayOfPromises) {
    console.log(p)
  } 
  console.log('end of createIteratorsAwaitAndPrint')
}
createIteratorsAwaitAndPrint()

Here we do the await in the for construct. Notice the "end of [...]" log doesn't happen until we have finished iterating.

This is exactly why people has to stay away or at least think twice before considering to use the async await abstraction since it's an abstraction over another abstraction.

What you should simply do is to stay in the sync timeline and attach a .then(v => console.log(v)) stage to your promises to print out their results in the order that you expect.

Like;

var ps = Promise.all([p1,p2,p3].map(p => p.then(v => (console.log(v), v))));

There isn't much sense in using a for await loop within an async function as well. You don't need an async function here but even if you use one then you are expected to return the arrayOfPromises array to do your looping on it's values in the sync code. The for await loop is just fine in the synchronous code. Recent JS engines allow you to use the for await loop without declaring an async function in a module. It takes an async iterable (preferably but also sync as well) and autoconverts the .next() resolving promise in the async iterable into it's naked resolution value to be processed in the loop. So it will wait until the first array item to resolve before proceeding to the next one regardless the following promises had already resolved or not.

Now in the sync part of your code you can do;

for await (let v of ps){
  console.log(v);
}

which will log the values in the order of appearance in the array.

Related