71

let x = 0;

async function test() {
    x += await 5;
    console.log('x :', x);
}

test();
x += 1;
console.log('x :', x);

The values of x logged are 1 and 5. My question is: why is the value of x 5 on second log?

If the test is executed after x += 1 (since it is an async function) then the value of x is 1 by the time test is executed, so x += await 5 should make the value of x 6.

FZs
  • 16,581
  • 13
  • 41
  • 50
Aldrin
  • 2,036
  • 15
  • 12

5 Answers5

66

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 are 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 is an already resolved promise (or as in here, not a promise at all - these will be converted to resolved promises by await), so the following will return 1 (from the main script) and 6 (from the function), as you expected:

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) (unless x is an expression with side-effects, which isn't the case here). I'll use the latter form for to make it easier to understand:

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 evaluates the expression inside await (5), turns it into a resolved promise, and starts waiting for it.

The code after the function call starts to run, and modifies the value of x (from 0 to 1), then logs it.

x is now 1.

Then, after the main script finishes, the interpreter goes back to the paused test function, and continues evaluating the line, which, with the await out of the way, looks like this:

x = (0 + 5);

And, since the value of x has already been 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);
    console.log(new Error().stack.replace('Error', 'Stacktrace')); //Log stacktrace using an Error object
    return x;
  },
  set z(value) {
    console.log('set x =', value);
    console.log(new Error().stack.replace('Error', 'Stacktrace')); //Log stacktrace using an Error object
    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);
console.log('end of main script')

/* Output:

inside async function
get x : 0 <-------------- async fn reads
Stacktrace
    at Object.get z [as z] (https://stacksnippets.net/js:19:17)
    at test (https://stacksnippets.net/js:31:3) <-- async fn is synchronous here
    at https://stacksnippets.net/js:35:1 <--------- (main script is still in the stack)

main script
get x : 0
Stacktrace
    at Object.get z [as z] (https://stacksnippets.net/js:19:17)
    at https://stacksnippets.net/js:37:1
set x = 1
Stacktrace
    at Object.set z [as z] (https://stacksnippets.net/js:24:17)
    at https://stacksnippets.net/js:37:5
x : 1
end of main script

set x = 5 <-------------- async fn writes
Stacktrace
    at Object.set z [as z] (https://stacksnippets.net/js:24:17)
    at test (https://stacksnippets.net/js:31:7) <-- async fn is asynchronous (main script is no longer in the stack)
x : 5 <------------------ async fn logs

*/
/* Just to make console fill the available space */
.as-console-wrapper {
  max-height: 100% !important;
}
FZs
  • 16,581
  • 13
  • 41
  • 50
  • Maybe worth noting: *"You probably know, that `x += y` is identical to `x = (x + y)`."* - This is not the case in every situation in every language, but in general you can count on them acting the same. – Nick is tired Jan 19 '20 at 05:58
  • It's wrong explanation. Main problem consist in returning result from microtask. I can easy show why your explanation such as many others here are incorrect. For example we have this code: `const asyncA = async () => { console.log("start"); const result = await new Promise((resolve) => { console.log("Hello I am code executing before suspending"); resolve("aftermath") }); console.log("end"); return result }; asyncA(); console.warn("returned into global code after suspending async function");` – MaximPro Nov 30 '21 at 21:46
  • @MaximPro I think we are saying the same thing, just in different words. What makes you think I'm wrong? Which part is it in my answer you don't agree with? I'm open to constructive criticism. – FZs Dec 01 '21 at 14:04
  • @FZs probably you may be right, anyway the best decision on the current part of time it's spec tc39. There is link `https://tc39.es/ecma262/#sec-async-function-definitions-runtime-semantics-evaluation`. Before suspending async code it will be complete the first 2 steps and just after that in step 3 will occur suspending async code. I think you understand what I mean by that, and why `x += await 5` not equal `x = x + await 5`. P.S Of course I wanted create another one reply to this question, but unfortunately I dont have enough time for that. – MaximPro Dec 01 '21 at 20:56
  • @FZs I want add additional thing. You've written `x = (x + await 5);` and after `x = (0 + await 5);` But we have original expression and it's `x += await 5` it will be equal `x = await (x + 5)` and this will be absolutely right. Because, try to complete such expression: `console.log(1); await console.log(2); console.log(3)`. By your logic `console.log` in `await` will be deferred. But it is not. You will see `1,2,3` by order. Because even expression in `await` will be calculated before suspending. Fin :) – MaximPro Dec 01 '21 at 21:10
  • @MaximPro `x += await 5` **is equivalent** to `x = x + await 5` (unless `x` had been an expression with side effects, which it isn't), not to `x = await (x + 5)` (it would only make a difference with real promises, `await` is almost no-op for non-promises). Compare `let x = 1; x += await Promise.resolve(2)` to `let x = 1; x = x + await Promise.resolve(2)` vs `let x = 1; x = await (x + Promise.resolve(2))`. The latter will concatenate `x` with the promise object, leaving you with `1[object Promise]`, while the first two give you `3`. – FZs Dec 02 '21 at 23:34
  • 1
    @MaximPro "*By your logic `console.log` in `await` will be deferred.*" Maybe I wasn't clear about this, but I didn't mean that. That didn't matter in the code in question, as the expression `5` has no side effects. But as `5` (or `console.log(2)`) is inside of (the operand of) `await`, it must be evaluated first, then *the result of that expression* will be `await`ed, hence deferred. All the same, the expression *in which the `await` is* (e.g. `console.log(await 2)` or even `await 0, console.log(2)`), **will** be deferred. I'll edit my answer soon to note that. – FZs Dec 02 '21 at 23:42
  • @FZs now, I am liked, this sounds confident that you understand what about I was saying. Well, it would be nice if you did editing your post with due our little conversation. – MaximPro Dec 03 '21 at 04:56
  • @MaximPro I'll do that when I have some free time; I wrote my previous reply at 1AM and I neither do I have time now. I'll notify you when it's done ;) – FZs Dec 03 '21 at 06:02
  • @MaximPro I've just edited my answer, check it out! – FZs Dec 03 '21 at 18:17
  • 1
    @FZs nicely done! Now this looks like better. – MaximPro Dec 04 '21 at 01:11
12

Your statement x += await 5 desugars to

const _temp = x;
const _gain = await 5;
x = _temp + _gain;

The _temporary value is 0, and if you change x during the await (which your code does) it doesn't matter, it gets assigned 5 afterwards.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
9

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

  1. Fetch the value of x. At the time of the execution, that's 0.
  2. await the next expression which is 5. So, function pauses now and will be resumed later.
  3. Resume the function. Expression is resolved as 5.
  4. Add the value from 1. and the expression from 2/3: 0 + 5
  5. Assign the value from 4. to 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
VLAZ
  • 26,331
  • 9
  • 49
  • 67
3

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.

Pranav C Balan
  • 113,687
  • 23
  • 165
  • 188
0

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.

Qonvex620
  • 3,819
  • 1
  • 8
  • 15
  • 1
    "*Meaning everything changes on the variable outside of it will not change the value inside of it after it was called*": that's not true. Async functions **do** receive variable changes during their execution. Just try this: `let x="Didn't receive change"; (async()=>{await 'Nothing'; console.log(x); await new Promise(resolve=>setTimeout(resolve,2000)); console.log(x)})(); x='Received synchronous change'; setTimeout(()=>{x='Received change'},1000)` It outputs `Received synchronous change` and `Received change` – FZs Jan 22 '20 at 17:19