Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 288
knona Avatar asked Mar 07 '20 10:03

knona


People also ask

How do you resolve promises on a map?

Combining And Resolving all Promises with Promise. all() , map() and Async/Await. So the first promise will be resolved then all the promises returned from the getProductId() for each product and then finally all the promises returned from capitalizeId() for each ID returned from the previous promises.

What does bad assignment mean in JavaScript?

The "Bad assignment" error (and the alternative "Invalid left-hand side in assignment" error) are thrown when JSLint, JSHint or ESLint encounters an assignment expression in which the left-hand side is a call expression.

What is the meaning of +=?

The addition assignment operator ( += ) adds the value of the right operand to a variable and assigns the result to the variable.

What does *= mean in JavaScript?

The multiplication assignment operator ( *= ) multiplies a variable by the value of the right operand and assigns the result to the variable.


1 Answers

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.

like image 68
CertainPerformance Avatar answered Sep 28 '22 04:09

CertainPerformance