Assignment operator, map and promises. What's wrong with that code ? Javascript
Asked Answered
C

1

6

I was doing some stuff and I ran into a problem that I can't understand. I simplified the code to get that:

function somePromise() {
    return new Promise((resolve, reject) => {
        resolve(1);
    });
}

async function main() {
    let count = 0;
    const arr = [1, 2, 3, 4, 5];
    const promises = arr.map(async () => {
        count += await somePromise();
    })
    await Promise.all(promises);
    console.log(count);
}

main().then(() => console.log('Done'));

What result do you expect ?

1
Done

is logged.

When I change

count += await somePromise();

to

const nb = await somePromise();
count += nb;

I get

5
Done

what I expected for the first time.

Can you help me to find what is wrong ? I don't get it.

Contradictory answered 7/3, 2020 at 10:11 Comment(0)
S
6

When the interpreter comes across the await, it will pause the function until the resolution of the Promise. Even if the Promise resolves immediately, the function will only resume during the next microtask. In contrast, the array is iterated through immediately, synchronously. When you do

const promises = arr.map(async () => {
    count += await somePromise();
})

After the array is iterated through, but before the awaits have resolved, the "current" value of count which is taken for use by the += is retrieved before the await resolves - and the value of count before then is 0. So, it looks to the interpreter as if there are 5 separate statements:

count += await somePromise();
count += await somePromise();
count += await somePromise();
count += await somePromise();
count += await somePromise();

which resolve to something like

const currentValueOfCount = count;
count = currentValueOfCount + await somePromise();
count = currentValueOfCount + await somePromise();
count = currentValueOfCount + await somePromise();
count = currentValueOfCount + await somePromise();
count = currentValueOfCount + await somePromise();

So, each time, the right-hand side of the = resolves to 0 + 1, so at the end of the loop, count is only 1.

If you're interested where this is described in the specification, look at the semantics for assignment operators. Where += is one of the AssignmentOperators, the following syntax:

LeftHandSideExpression AssignmentOperator AssignmentExpression

does:

  1. Let lref be the result of evaluating LeftHandSideExpression.
  2. Let lval be ? GetValue(lref).
  3. Let rref be the result of evaluating AssignmentExpression.
  4. Let rval be ? GetValue(rref).
  5. Let op be the @ where AssignmentOperator is @=.
  6. Let r be the result of applying op to lval and rval as if evaluating the expression lval op rval.

See how lval is retrieved immediately, before the right-hand side of the operator is evaluated. (If lval had been retrieved after the right-hand side, the AssignmentExpression, is evaluated, the results would have been 5, as you're expecting)

Here's an example of this behavior without asynchronous operations:

let num = 5;
const fn = () => {
  num += 3;
  return 0;
}
num += 2 + fn();
console.log(num);

Above, the num += 2 + fn(); retrieves num as 5 immediately for use in +=, then calls fn(). Although num is reassigned inside fn, it doesn't have any effect, because the value of num has already been retrieved by the outer +=.


With your working code, when you do

const nb = await somePromise();
count += nb;

This will put the resolve value of somePromise into the nb variable, and then count += nb; will run. This behaves as expected because the "current" value of count used for += is retrieved after the Promise resolves, so if a prior iteration reassigned count, it'll be successfully taken into account by the next iteration.

Sculpt answered 7/3, 2020 at 10:22 Comment(1)
Thank you very much for your impressive detailed response! I understand nowContradictory

© 2022 - 2024 — McMap. All rights reserved.