Skip to content Skip to sidebar Skip to footer

Why Async/await Have Different Output When Await Expression Is A Argument Of Concat()?

I am confused about the examples below. I understand why output2 is [1000,2000,3000] because of the closure and that's why all the async functions inside map() update the same arra

Solution 1:

Looking at the statement

 output1 = output1.concat([await sleep(sec)]);

On the left hand side output1 is a variable identifier, used to provide the location in which to store the result of evaluating the right hand side. The variable's binding doesn't change and it always supplies the location of the variable's value.

On the right hand side output1 is a value - the value retrieved from the location supplied by the variable name.

Now if the JavaScript engine retrieves the value of output1 before continuing evaluation, all three map function calls

  • retrieve a reference to the empty array stored in output,
  • wait for the timer promise and set output1 to a new value being the array returned from the concat method.

Hence each map operation concatenates an array containing a timer value to an empty array and stores the result in output1 overwriting the result of previous awaited operations.

This explains why you only see the last array stored in output1 when Promise.all becomes settled. I will also retract the "if the JavaScript engine..." wording above. The JavaScript Engine does get the value of output1 before the await:

function sleep(ms) {
  return new Promise(resolve =>
    setTimeout(() => {
    console.log( output1.length);
      resolve(ms);
    }, ms)
  );
}

const seconds = [1000, 3000, 2000];

let output1 = [];
let output2 = [];

(async function run1() {
  await Promise.all(
    seconds.map(async sec => {
      output1 = output1.concat([await sleep(sec)]);
      //output1.push(await sleep(sec));
      console.log(output1[0]);
    })
  );
  console.log({ output1 });
})();

let count = 6;
let timer = setInterval( ()=> {
  console.log(output1[0])
  if(--count <=0 ) {
     clearInterval( timer);
  }
}, 500);

To clear up why the second method (run2) works (which is not related to the existence of closures):

The .map method calls the map function synchronously and the calls synchronously return a promise without waiting for timer promise fulfillment.

In the second version,

seconds.map(async sec => {
  const res = await sleep(sec);
  output2 = output2.concat([res]);
}

the const res = await sleep( sec) line saves the execution context and waits for the sleep promise to fulfill. When the promise is fulfilled, await restores the saved context and stores the promise value in res. The next line

  ouput2 = output2.concat([res]);

executes after timer expiry and on the right hand side will load the value of output2 current when the line is executed, as updated by a previous timer expiry if one has occurred.

Contrast this with run1 where the JavaScript engine essentially cached* the value of ouput1 when starting to evaluate the expression on the right hand side of the assignment operator and used the same empty array value for all iterations as demonstrated in the snippet.

*The accepted answer of the duplicate indicates that the left hand operand of an addition operation is retrieved from storage before the right hand operand is returned by await. In the case of run1 we are seeing that the object on which a method will be called (the value of output1) is retrieved before the value of the argument used to call the method has been determined. As described in comments to the linked answer, this is quite a "hidden pitfall".


Solution 2:

I add a line in between first example. Guess this will help you see the reason.

"use strict";

function sleep(ms) {
  return new Promise(resolve =>
    setTimeout(() => {
      resolve(ms);
    }, ms)
  );
}

const seconds = [1000, 3000, 2000];

let output1 = [];

(async function run1() {
  await Promise.all(
    seconds.map(async sec => {
      const dummy = output1;
      console.log(`dummy_${sec}`, dummy);
      output1 = dummy.concat([await sleep(sec)]);
    })
  );
  console.log({ output1 });
})();

Update:

Below is your original code transpiled to ES5 code, with generator polyfill. Pay attention to how transpiler manages to pause between the output1.concat([ /* pause here */ ]) call.

The trick is to bind .concat in-place to output1, before yield and wait for continuation.

At the moment this binding happens, output1 == [], this is what I wanna emphasis by re-assigning to dummy variable and print it in previous code.

var seconds = [1000, 3000, 2000];
var output1 = [];
(function run1() {
    return __awaiter(this, void 0, void 0, function () {
        var _this = this;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0: return [4 /*yield*/, Promise.all(seconds.map(function (sec) { return __awaiter(_this, void 0, void 0, function () {
                        var _a, _b;
                        return __generator(this, function (_c) {
                            switch (_c.label) {
                                case 0:
                                    _b = (_a = output1).concat;
                                    return [4 /*yield*/, sleep(sec)];
                                case 1:
                                    output1 = _b.apply(_a, [[_c.sent()]]);
                                    return [2 /*return*/];
                            }
                        });
                    }); }))];
                case 1:
                    _a.sent();
                    console.log({ output1: output1 });
                    return [2 /*return*/];
            }
        });
    });
})();

Post a Comment for "Why Async/await Have Different Output When Await Expression Is A Argument Of Concat()?"