Your statement x += await 5
desugars to
const _temp = x;
await;
x = _temp + 5;
The _temp
orary value is 0
, and if you change x
during the await
(which your code does) it doesn't matter, it gets assigned 5
afterwards.
TL;DR: Because +=
reads x
before, but writes it after it has changed, due to the await
keyword in its second operand (right-hand side).
async
functions run synchronously when they called until the first await
statement.
So, if you remove await
, it behaves like a normal function (with the exception that it still returns a Promise).
In that case, you get 5
(from the function) and 6
(from the main script) in the console:
let x = 0;
async function test() {
x += 5;
console.log('x :', x);
}
test();
x += 1;
console.log('x :', x);
The first await
stops synchronous running, even if its argument available synchronously, so the following will return 1
(from the main script) and 6
(from the function), as you expect:
let x = 0;
async function test() {
// Enter asynchrony
await 0;
x += 5;
console.log('x :', x);
}
test();
x += 1;
console.log('x :', x);
However, your case is a bit more complicated.
You've put await
inside an expression, that uses +=
.
You probably know, that in JS x += y
is identical to x = (x + y)
. I'll use the latter form for better understanding:
let x = 0;
async function test() {
x = (x + await 5);
console.log('x :', x);
}
test();
x += 1;
console.log('x :', x);
When the interpreter reaches this line...
x = (x + await 5);
...it starts evaluating it, substitutes x
, so it turns to...
x = (0 + await 5);
...then, it reaches the await
and stops.
The code after the function call starts to run, and modifies the value of x
, then logs it.
x
is now 1
.
Then, after the main script exits, the interpreter goes back to the paused test
function, and continues evaluating that line:
x = (0 + 5);
And, since the value of x
is already substituted, it remains 0
.
Finally, the interpreter does the addition, stores 5
to x
, and logs it.
You can check this behaviour by logging inside an object property getter/setter (in this example, y.z
, which reflects the value of x
:
let x = 0;
const y = {
get z() {
console.log('get x :', x);
return x;
},
set z(value) {
console.log('set x =', value);
x = value;
}
};
async function test() {
console.log('inside async function');
y.z += await 5;
console.log('x :', x);
}
test();
console.log('main script');
y.z += 1;
console.log('x :', x);
/* Output:
inside async function
get x : 0 <-- async fn reads
main script
get x : 0
set x = 1
x : 1
set x = 5 <-- async fn writes
x : 5 <-- async fn logs
*/
/* Just to make console fill the available space */
.as-console-wrapper {
max-height: 100% !important;
}
Ya its little bit tricky what's actually happening is both addition operations are happening parellaly so the operation would be like :
Within promise : x += await 5
==> x = x + await 5
==> x = 0 + await 5
==> 5
Outside : x += 1
==> x = x + 1
==> x = 0 + 1
==> 1
since all above operations are happening left to right the first part of addition may be calculated at the same time and since there is an await before 5 that additio may delay a bit. You can see the execution by putting breakpoint within the code.
Async and Await are extensions of promises. An async function can contain an await expression that pauses the execution of the async function and waits for the passed Promise's resolution, and then resumes the async function's execution and returns the resolved value. Remember, the await keyword is only valid inside async functions.
Even if you have changed the value of x after calling the test function, still the value of x will remain 0 cause async function has already created it's new instance. Meaning everything changes on the variable outside of it will not change the value inside of it after it was called. Unless if you put your increment above the test function.
This code is quite complex to follow because it takes some unexpected async jumps back and forth. Let's examine (close to) how it's actually going to be executed and I'll explain why afterwards. I've also changed the console logs to add a number - makes referring to them easier and also shows better what is logged:
let x = 0; // 1 declaring and assigning x
async function test() { // 2 function declaration
x += await 5; // 4/7 assigning x
console.log('x1 :', x); // 8 printing
}
test(); // 3 invoking the function
x += 1; // 5 assigning x
console.log('x2 :', x); // 6 printing
So, the code is not actually going in a straight manner, that's for sure. And we have a weird 4/7
thing as well. And that is really the entirety of the problem here.
First of all, let's clarify - async functions are not actually strictly asynchronious. They would only pause the execution and resume later if the await
keyword is used. Without it, they execute top to bottom, expression after expression synchronously:
async function foo() {
console.log("--one");
console.log("--two");
}
console.log("start");
foo();
console.log("end");
async function foo() {
console.log("--one");
await 0; //just satisfy await with an expression
console.log("--two");
}
console.log("start");
foo();
console.log("end");
So, the first thing we need to know that using await
will make the rest of the function execute later. In the given example, that means that console.log('x1 :', x)
is going to executed after the rest of the synchronous code. That's because any Promises will be resolved after the current event loop finishes.
So, this explains why we get x2 : 1
logged first and why x2 : 5
is logged second but not why the latter value is 5
. Logically x += await 5
should be 5
...but here is the second catch to the await
keyword - it will pause the execution of the function but anything before it has already run. x += await 5
is actually going to be processed in the following manner
x
. At the time of the execution, that's 0
.await
the next expression which is 5
. So, function pauses now and will be resumed later.0 + 5
x
So, the function pauses after it read that x
is 0
and resumes when it's already changed, however, it doesn't re-read the value of x
.
If we unwrap the await
into the Promise
equivalent that would execute, you have:
let x = 0; // 1 declaring and assigning x
async function test() { // 2 function declaration
const temp = x; // 4 value read of x
await 0; //fake await to pause for demo
return new Promise((resolve) => {
x = temp + 5; // 7 assign to x
console.log('x1 :', x); // 8 printing
resolve();
});
}
test(); // 3 invoking the function
x += 1; // 5 assigning x
console.log('x2 :', x); // 6 printing