Skip to content Skip to sidebar Skip to footer

Why Is Awaiting Inline On A Variable In The Outer Scope Different From Simple Await Assignment?

Before I asked this question, I did research this question. I don't understand why in the examples below the output is different in run1 and run2.

Solution 1:

For the first case (output1), the current value of output1 is "cached" before calling and evaluating the async function and awaiting it.

It would be more or less equivalent to the following (order of execution follows the sleep time of each async function):

let old= output1; //0
output1 =old+1000;
output1 =old+2000;
output1 =old+3000; //0+3000is the finalresult

For the second case (output2), the async function is evaluated before the current value of output2 is cached. You end up with a sum of all values.

It is more or less equivalent to:

let old= output2; //0
output2 =old+1000;
old= output2; //1000
output2 =old+2000;
old= output2; //3000
output2 =old+3000; //3000+3000is the finalresult

Note: if you use the values [1000, 4000, 2000], you will see that the result is 4000, so only the middle value is added to the output (not 1000+2000 which would also be 3000 with the values from the question)

To be honest, I would have expected the result in the first case to be either 2000 (the last value) or non-deterministic (race-conditions). Apparently, there is some little detail in how JavaScript schedules each async task.

Thanks to A_A with an answer to why the final result is 3000 (or 4000, with updated values): this is the longest delay and it will be executed/finished last.


Additional explanation: there seems to be confusion caused by my bad word choice of "cached". Let me try to explain:

JavaScript programs are generally evaluated top-to-bottom, left-to-right (ignoring async calls for a moment).

let y = 6;
let x = 7;
y = y * x;

Will be "seen" by the JavaScript runtime as:

  1. Store 6 in y
  2. Store 7 in x
  3. Remember current y value (6)
  4. Remember current x value (7)
  5. Multiply 6 and 7 (= 42)
  6. Store 42 in y

Let's map this to your first example:

for (let i of [100,300,200])
   output1 = output1 + (await sleep(i));
  1. Remember current output1 value (0)
  2. Start first async "sleep" task and await its completion
  3. Remember current output1 value (0)
  4. Start second async sleep task and await its completion
  5. Remember current output1 value (0)
  6. Start third async sleep task and await its completion
  7. First sleep task completes (100)
  8. Add current output1 value and task result (0 + 100)
  9. Store result in output1 (100)
  10. Third sleep task completes (200)
  11. Add current output1 value and task result (0 + 200)
  12. Store result in output1 (200)
  13. Second sleep task completes (300)
  14. Add current output1 value and task result (0 + 300)
  15. Store result in output1 (300)
  16. The final value of output1 is 300

Now compare to the second example. The evaluation order becomes:

for (let i of [100,300,200]) {
  const res = await sleep(sec);
  output2 = output2 + res;
}
  1. Start first async "sleep" task and await its completion
  2. Start second async sleep task and await its completion
  3. Start third async sleep task and await its completion
  4. First sleep task completes (100)
  5. Remember current output2 value (0)
  6. Add current output2 value and task result (0 + 100)
  7. Store result in output2 (100)
  8. Third sleep task completes (200)
  9. Remember current output2 value (100)
  10. Add current output2 value and task result (100 + 200)
  11. Store result in output2 (300)
  12. Second sleep task completes (300)
  13. Remember current output2 value (300)
  14. Add current output2 value and task result (300 + 300)
  15. Store result in output2 (600)
  16. The final value of output2 is 600

Solution 2:

On top of @ef-dknittl-frank's answer. I would like to share my point of view. I have difficulties understanding this question because I want to understand how exactly JS working behind the scene.

TL;DR, if you rewrite the run1 and run2 in .then, it would be more clear.

run1 can be rewritten as below with .then():

(asyncfunctionrun1() {
  awaitPromise.all(
    seconds.map(sec => {
      returnPromise.resolve(output1).then(temp => {
        returnsleep(sec).then(res => {
          output1 = temp + res;
        });
      });
    })
  );
  console.log({ output1 });
})();

run2 can be rewritten as below with .then():

(asyncfunctionrun2() {
  awaitPromise.all(
    seconds.map(sec => {
      returnsleep(sec).then(res => {
        output2 = output2 + res;
      });
    })
  );
  console.log({ output2 });
})();

I post this answer just in case you have difficulties like me to understand this question.

The key is what execution context (local scope, scope chain and variables in closures from outer scopes) is when the codes are executing.

For the run1,

We rewrite the below codes:

output1 = output1 + (awaitsleep(sec));

to

a = b + c

The sequence of execution is:

1. store the value tob2. store the value to c
3. sum b and c
4. assign the sum (b+c) toa

we analyze the sequence one by one.

1. store the value tob

When we execute line 1, the value of output1 is assigned to b. At this point, output1 is 0 from the scope chain(outer scope) and only sync codes are running. Therefore, all the value of b is 0 inside the map() loop

2. store the value to c

When we execute line 2, the value of await sleep(sec) is assigned to c. For instance, let say 1000(From await sleep) is assigned to c. At this point, you can already see the log start and fin because all sync code(outer scope) is finished before executing line2.

3.sum b and c

When we execute line 3, b is 0 and c is 1000(from the previous example), so the sum is 1000.

4. assign the sum (b+c) toa

When we execute line 4, the a is output1 and output1 is the variable identifier, used to provide the location in which to store a value and in this case, it stores the sum. To be more clear, the line4 is executing after all sync code(outer scope) is finished, so we access the output1 by Closures from outer scopes.

Why output1 always show 3000? It is because the line4 above is executed asynchronously and the last code executed is 3000ms. a(output1) is re-assigned by a new value from the asynchronous codes.

run1 can be rewritten as below with .then():

(asyncfunctionrun1() {
  awaitPromise.all(
    seconds.map(sec => {
      returnPromise.resolve(output1).then(temp => {
        returnsleep(sec).then(res => {
          output1 = temp + res;
        });
      });
    })
  );
  console.log({ output1 });
})();

For the run2,

The codes below

output2 = output2 + res;

Both output2 are referenced to the same thing and they are accessed also by Closures from outer scopes. To be more clear, this line is executing after all sync code(outer scope) is finished, so we access the output2 by Closures from outer scopes.

run2 can be rewritten as below:

(asyncfunctionrun2() {
  awaitPromise.all(
    seconds.map(sec => {
      returnsleep(sec).then(res => {
        output2 = output2 + res;
      });
    })
  );
  console.log({ output2 });
})();

Extra point:

if we change the codes of run1 below

from:

output1 = output1 + (awaitsleep(sec));

to:

output1 = (awaitsleep(sec)) + output1;

It will output the same result as run2.

Let's rewrite it to a = b + c like the previous example.

The sequence of execution is:

1. await sleep() and store the value to b (it is running asynchronously) 
2. store the value to c (c is equal to `output1` which is accessed by Closure)
3. sum b and c
4. assign the sum (b+c) to a (a is equal to `output1` which is accessed by Closure)

If we rewrite the above changes of run1 to .then():

(asyncfunctionrun1() {
  awaitPromise.all(
    seconds.map(sec => {
      returnsleep(sec).then(res => {
        output1 = output1 + res;
      });
    })
  );
  console.log({ output1 });
})();

Did you notice? it is the same as run2().

Post a Comment for "Why Is Awaiting Inline On A Variable In The Outer Scope Different From Simple Await Assignment?"