0

Lets say that I have this code:

const array = [1, 2, 3]

let counter = 0

array.map(async (item) => {
  console.log(await item, ++counter)
  console.log(await item, ++counter)
})

the expected output would be

1, 1
1, 2
2, 3
2, 4
3, 5
3, 6

but what I am getting is this

1, 1
2, 2
3, 3
1, 4
2, 5
3, 6

it seems like the first await call is running first for the whole array, then the second one in being run, why is this happening?

GamesMan
  • 1
  • 3
  • Why do you expect the initial output? – evolutionxbox May 07 '21 at 13:47
  • 1
    And why would you need to `await` a number? – Pointy May 07 '21 at 13:48
  • it is just part of a bigger problem simplified @Pointy – GamesMan May 07 '21 at 13:48
  • It's simplified to the point of not being useful. – VLAZ May 07 '21 at 13:49
  • What happens if you take away the `await`? When you try to "simplify" a problem involving behavior you don't understand, it's always risky because the mystery may lie in parts of your code that you don't expect. – Pointy May 07 '21 at 13:50
  • @Pointy, if the `await` was removed it would work as expected, you are completely right but I am trying to understand this behavior as it is. – GamesMan May 07 '21 at 13:53
  • 1
    Async functions pause at `await`. That's the entirety of the explanation about your question. But is it really useful to solve your problem? You most likely shouldn't be using an async in a `.map()`. Unless you put that in a `Promise.all()` – VLAZ May 07 '21 at 13:56
  • Right, the basic array functions don't understand `async` callbacks. The `.map()` call will just build an array of Promise objects. – Pointy May 07 '21 at 13:59
  • @VLAZ, even if it pauses shouldn't it result to the expected output? – GamesMan May 07 '21 at 13:59
  • @Pointy yes it would, but why I am getting this output thought? – GamesMan May 07 '21 at 14:00
  • If you want to understand your output, you might want to read up on what microtasks are -> https://javascript.info/event-loop , on a side note async / await on a map/foreach/reduce etc, generally doesn't make much sense. – Keith May 07 '21 at 14:01
  • pause means the function stops and the next thing in the queue continues. Which is the next function. So you get: start 1 -> pause 1 -> start 2 -> pause 2 -> start 3 -> pause 3 -> resume 1 -> pause 1 -> resume 2 -> pause 2 -> resume 3 -> pause 3 -> resume 1 -> finish 1 -> resume 2 -> finish 2 -> resume 3 -> finish 3 – VLAZ May 07 '21 at 14:02
  • @VLAZ, Aha thank you it is much more clearer now. – GamesMan May 07 '21 at 14:04
  • 1
    @Keith, thank you I will sure read that. – GamesMan May 07 '21 at 14:04
  • @GamesMan except it doesn't really help with any real code. Because you shouldn't write this code to begin with. And your real use-case is probably different. – VLAZ May 07 '21 at 14:07
  • @VLAZ, it is different, but this does not nullify this question. – GamesMan May 07 '21 at 14:10
  • Does this answer your question? [Use async await with Array.map](https://stackoverflow.com/questions/40140149/use-async-await-with-array-map) – Charlie May 07 '21 at 14:29

3 Answers3

2

Here's a (hopefully) better illustration. The thing is, map doesn't wait for its callback ("mapper") to complete. It just fires it for each element. So, when map is done, we end up with N mappers hanging around, each of them waiting for await to complete, because await, even if used with a non-promise, still means waiting, namely, waiting for the current "execution context" to exit.

const array = ['A', 'B', 'C']

let counter

function item(arg) {
    console.log('    CALLED item', arg, ' => ', counter++)
}

console.log('SYNC: before map')
counter = 0
array.map(async (x) => {
    console.log('  begin SYNC mapper', x)
    item(x);
    console.log('    1st item of', x, 'done')
    item(x);
    console.log('  end SYNC mapper', x)
})
console.log('after map')

console.log('--------------------------')

console.log('ASYNC: before map')
counter = 0
array.map(async (x) => {
    console.log('  begin ASYNC mapper', x)
    await item(x);
    console.log('    1st item of', x, 'done')
    await item(x);
    console.log('  end ASYNC mapper', x)
})
console.log('after map')

console.log('execution context ended')
.as-console-wrapper { max-height: 100% !important; top: 0; }
georg
  • 211,518
  • 52
  • 313
  • 390
1

Array methods like map, forEach etc are not await-aware. In other words, they would execute the callback function regardless of the await keywords used inside them.

Here is a test:

const array = [1, 2, 3]

array.map(async (item) => {
  console.log('Immediate Item: ', item);
  console.log('Awaited item', await Promise.resolve(item));
})

You can see from the results that all 3 "immediate items" are loged out first and then comes the "awaited" items.

If you need a loop that respects async/await, use for-of loop.

async function main() {

  const array = [1, 2, 3]

  for (let item of array) {
          console.log('Immediate Item: ', item);
          console.log('Awaited item', await Promise.resolve(item));
    }

}

main();
Charlie
  • 22,886
  • 11
  • 59
  • 90
-1

Besides the asynchronous part

Don't use Array.map as an imperative loop. Use Array.forEach instead.

  • Array.map transforms each value (in any order (!)).
  • Array.forEach calls each value in sequential order.

Practically speaking, I guess you could assume sequential execution for Array.map, but it is bad practice to do that.

The asynchronous part

You are expecting Array.map to wait for the two console.log statements to finish, before calling the next item. This is neither true for Array.map or Array.forEach, because though the code might look sequential, it is asynchronous. And in Jav

If you want to execute asynchronous functions in sequence, there are basically two ways of doing it:

Use for loop

(async () => {
  for(var i=0; i<array.length; i++) {
    console.log(await array[i], ++counter)
    console.log(await array[i], ++counter)
  }
})()

Use Array.reduce

array.reduce(async (p, item) => {
  await p
  console.log(await item, ++counter)
  console.log(await item, ++counter)
}, Promise.resolve())

Mal
  • 355
  • 2
  • 8
  • It's maybe worth pointing out the `Array.reduce`, creates something called a Promise chain,. Were as the for loop doesn't, for very large arrays the for loop would be better, and is more flexible as break / continue also work as expected. – Keith May 07 '21 at 15:55