4
(async function iife () {
  const numbers = [1, 2, 3, 4]
  let count = 0
  async function returnNumberAsync (number) {
    return new Promise(resolve => {
      setTimeout(() => resolve(number), 0)
    })
  }
  await Promise.all(numbers.map(async number => {
    count += await returnNumberAsync(number)
  }))
  console.log(count)
})()

This snippet logs 4 to the console, which is completely beyond me. As soon as I assign the promised value inside map to its own local variable …

const result = await returnNumberAsync(number)
count += result;

… it logs 10 like I'd expect. What's happening when I count += await …??

panepeter
  • 2,421
  • 26
  • 33

3 Answers3

7

When you do count += await <expression>, the initial value of count to be added to the resolve value is saved before the await part is resolved. So

count += await returnNumberAsync(number)

is like

count = count + await returnNumberAsync(number)

as you can see:

(async function iife () {
  const numbers = [1, 2, 3, 4]
  let count = 0
  async function returnNumberAsync (number) {
    return new Promise(resolve => {
      setTimeout(() => resolve(number), 0)
    })
  }
  await Promise.all(numbers.map(async number => {
    count = count + await returnNumberAsync(number)
  }))
  console.log(count)
})()

In each of the 4 closures, count is seen to be 0 before the awaits resolve, so only in the final await microtask, where it resolves to

count += await returnNumberAsync(4)
// or
count = 0 + await returnNumberAsync(4)
// or
count = 4

does 4 get assigned to count. The fact that other numbers were assigned to count earlier is ignored. They did happen, but the assignment of the final iteration is all you see with the current code.

CertainPerformance
  • 313,535
  • 40
  • 245
  • 254
  • 2
    Thats really a hidden pitfall (yay, concurrency in a single threaded language) ... `count = await fn() + count;` would also work – Jonas Wilms May 23 '19 at 09:31
  • Ok, so the assignment of `count` happens before the function is interrupted, as it's placed left of the `await`. And if it's put right (or below) the `await`, everything's fine. Makes sense, but quite a pitfall nonetheless. Thanks everyone! – panepeter May 23 '19 at 16:13
1

You should always try to avoid mutating a shared state from within asynchronous code. If your operations are not atomic (count += await is not atomic as it reads count, waits for the asynchronous task, then writes to count (and count might have changed in the meantime)) that can get you into trouble.

Your code can easily be modified to not use a shared state:

 const count = (await Promise.all(numbers.map(returnNumberAsync))).reduce((a, b) => a + b, 0);
Jonas Wilms
  • 120,546
  • 16
  • 121
  • 140
0

It would be more clear what is happening if you rewrite the async/await inside the map() to .then.

function returnNumberAsync(number) {
  return new Promise(resolve => {
    setTimeout(() => resolve(number), 0);
  });
}

(async function iife() {
  const numbers = [1, 2, 3, 4];
  let count = 0;

  await Promise.all(
    numbers.map(number =>
      Promise.resolve(count).then(temp =>
        returnNumberAsync(number).then(res => {
          count = temp + res;
        })
      )
    )
  );
  console.log(count);
})();
ikhvjs
  • 4,417
  • 2
  • 6
  • 23