Async actions in array.map and for loop

Viewed 810

I realized when I do something like this:

for (const entity of someArr) {
  console.log('start now!')
  await doSomeAsycAction()
  console.log('waited X secs!')
}

It prints out:

start now!
waited X secs!
start now!
waited X secs!
...

But when I use map:

arr.map(async entity => {
  console.log('start now!')
  await doSomeAsycAction()
  console.log('waited X secs!')
})

It prints out:

start now!
start now!
start now!
...
waited X secs!
waited X secs!
waited X secs!
...

Can someone explain why this is the case?

2 Answers

The difference between the two flows is that the first one (using for, for..in or for..of) is running the loop iterations sequentially and the other one (using map, filter, reduce, forEach and so on) are running (in case async symbol is used somewhere in the mapping function) concurrently (kind of). In for loops, the next iteration must wait for the previous one to finish. This allows you to do some async operations relevant for the next iteration. In contrast, using the async methods runs each iteration independently, so you can't rely on other iterations in your current iteration. Those kind of functions receive a function as an argument and executes it immediately for every item in the array.

Think of every iteration as an independent promise execution. When running an async function, the await symbol tells that this operation might take a while (i.e I/O, DB calls, network operations...) and let's the code outside of the current executed function keep going (and resume later on, after the async call returns). The map function sees that the current iteration is busy and go on to the next one. Somewhen in the future it would resume and execute console.log('waited X secs!').

You can simulate the same behavior of async executions with for loop this way (would maybe help demonstrating the difference):

for (const entity of someArr) {
  (async () => {
    console.log('start now!')
    await doSomeAsycAction()
    console.log('waited X secs!')
  })()
}

The async-await syntax is working per function scope, and map is defining a new function scope (the function passed as the param to the function) just like the (anonymous) function that gets executed each iteration in my example. Wish it helps to understand.

One important thing to notice is that each iteration of the map doesn't return the mapped value you were expecting for, but a promise that will be resolved with this value. So if you try to rely on one of the mapped array values - you must add an await right before it, otherwise the value type would still be a promise. Take a look at the following example:

let arr = [1];
arr = arr.map(async entity => incrementAsync(entity));
console.log(arr[0]) // would print an unresolved Promise object
console.log(await arr[0]) // would print 2

Basic array prototype loop functions like forEach, map, filter, find etc. don't wait for next iteration.Their basic behavior is to iterate not awaiting. If you want use like waiting function then try to use like below

for (const event of events) {
  if (failure or conditional) {   
    continue;
  } 
}
Related