6

(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?

pilchard
  • 12,414
  • 5
  • 11
  • 23
imheretolearn1
  • 127
  • 1
  • 6
  • 2
    `await` doesn't return a promise. It waits for the promise to be resolved. – Barmar Jun 13 '21 at 17:45
  • Even though the promise might be resolved, the log doesnt happen until your for loop evaluates the promise. So your first promise waits three seconds, then resolves your first loop. By this time, the others are also resolved. Try making your first resolve 500 and the second 1000 and you will see the 500ms delay between them. In short, it checks them _in order_. When it resolves is not exactly the for loops issue. – somethinghere Jun 13 '21 at 17:46
  • 2
    `await p` is redundant. `for await` automatically awaits before assigning to `p` when looping over a sync iterator. – Barmar Jun 13 '21 at 17:50
  • @Barmar - Hey, but as soon as an await is encountered first a default promise is returned from the async function, isnt it ? Like in this code: async function f(){ let d = await something(); // at this point a pending promise has been returned // whatever code we have left will be run once the promise we are awaiting resolves } isnt that correct? – imheretolearn1 Jun 13 '21 at 17:57
  • @Noob - Hey, this doesnt talk about for await of tho. – imheretolearn1 Jun 13 '21 at 17:58
  • You don't even need an `async` function if you are going to use the `for await` loop. Just make your promise array and consume it with the `for await` loop in the synchronous timeline. – Redu Jun 13 '21 at 18:06
  • 2
    [Never use `for await` with an array of promises!](https://stackoverflow.com/questions/59694309/for-await-of-vs-promise-all) – Bergi Jun 13 '21 at 23:21

2 Answers2

0

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.

MBauerDC
  • 160
  • 8
  • Actually you shouldn't do either - they fail to do proper error handling. – Bergi Jun 13 '21 at 23:26
  • 1
    The form of this is generally not best practice - I just wanted to keep it close to the code in the question, to demonstrate the semantic difference on something that does requires only minimal rethinking. Naturally, if you're not merely demonstrating an isolated idea - you should always handle failures explicitly. – MBauerDC Jun 14 '21 at 07:10
-1

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.

Redu
  • 25,060
  • 6
  • 56
  • 76
  • 1
    "*There isn't much sense in using a `for await` loop within an `async` function*" - uh, a `for await` loop can **only** be used within an `async` function? It's not possible to use the syntax in synchronous code, since it needs to suspend execution - just like `await` (but multiple times). – Bergi Jun 13 '21 at 23:27
  • @Bergi You would be right but now kind of right. This is exactly why i say people should stay away from async await abstraction because now there is the [top level await](https://v8.dev/features/top-level-await) or [this question](https://stackoverflow.com/questions/61916839/deno-top-level-await). Well then yes it is still not a synchronous code but terribly resembles one. I like Deno though. – Redu Jun 14 '21 at 17:35
  • 2
    @Redu a top level `await` runs in an async module. It doesn't really detract much from what Bergi said. Point being that `for await` only matters in async context (if we have to be more highly formal here). Your answer says you shouldn't use it in async function (which is one type of async context). I'd read that as recommending to not use `for await` in any async context. Which...itself doesn't make much sense. Or are you suggesting that `for await` should be used in top level async modules but not in async functions? That doesn't make much sense, either. – VLAZ Jun 14 '21 at 17:50
  • @VLAZ Yes you have read it correctly. My sentences are not correct. But what is an async module? As I understand all modules now have top level await functionality am i wrong? Also most of the code are now reside in modules. – Redu Jun 14 '21 at 18:24
  • 1
    Top-level `await` makes the module async. It's transparent, though - if in the "foo" module you do `import { x } from "bar"` and the **bar** module uses `await` at the top-level, that means that the import will not resolve until the **bar** module finishes waiting for the promise. Which in turn makes the **foo** module implicitly wait. Top-level `await` makes modules behave like async functions - once you use `await` once, anything else in the chain also has to be async to handle that. However, the top-level `await` does that without requiring you to explicitly say each module is async. – VLAZ Jun 15 '21 at 00:31