0

The following code (yes I know it is not idiomatic) prints 1,2. But I expected it to print 2,1.

(async()=>{
    let resolve;
    new Promise((r)=>{
        resolve = r
    }).then(()=>console.log('1'))
    await resolve();
    console.log('2')
})()
1. (async()=>{
2.     let resolve;
3.     new Promise((r)=>{
4.         resolve = r
5.     }).then(()=>console.log('1'))
6.     await resolve();
7.     console.log('2')
8. })()

Expected control flow:

Line 1: instantiate an anonymous async function expression
Line 8: ...and immediately call it
Line 2: variable declaration
Line 3 & 4: Instantiate Promise, run the executor function, assign the variable
Line 5: configure the `then` with the callback
Line 6: Schedule evaluation of the `then` for the next microtask
Line 6 (contd.): `resolve()` synchronously returns `undefined`
Line 6 (contd.): `await` triggered with undefined (unsure of action) 
Line 7: Print `2`
Line 8: Pop execution contexts from stack
<on next microtask>
Line 5: Push execution context for anonymous function onto stack, print `1`

Why is this wrong? Does the async keyword schedule the expression to the right to be on the next microtask, keeping the order 1,2?

Ben Aston
  • 53,718
  • 65
  • 205
  • 331
  • `await` suspends the execution of the function until the promise is resolved and then resumes the function. You can look at how generators work – hawks Feb 03 '20 at 15:43
  • `resolve()` returns not a promise, but `undefined`. – Ben Aston Feb 03 '20 at 15:44
  • 2
    @Ben irrelevant. `await` will *always* pause the execution. – VLAZ Feb 03 '20 at 15:45
  • @VLAZ — Not "until the promise is resolved" though. – Quentin Feb 03 '20 at 15:45
  • @VLAZ the callback supplied to `then` (ie. prints `1`) will be called on the next microtask (correct me if I'm wrong). The printing of `2` would normally be synchronous... but it is an async function which presumably alters the flow. – Ben Aston Feb 03 '20 at 15:46
  • Ah, OK, *now* I see what you mean. Sorry, I was being thick - I wasn't reading this properly, it seems. – VLAZ Feb 03 '20 at 15:47
  • @VLAZ, so await will always schedule the evaluation of the expression on the RHS for async execution? – Ben Aston Feb 03 '20 at 15:47
  • 3
    @Ben yes. `await ; ; ` gets changed to similar to return `new Promise((resolve) => {; resolve()}`. You know - basically the rest of the code is turned to a Promise. It doesn't matter if the awaited expression is a Promise or not. I assume is to maintain consistency - otherwise if you do `await foo` it would work *differently* depending on whether `foo` is a Promise or not. – VLAZ Feb 03 '20 at 15:52
  • @VLAZ I will accept that as an answer – Ben Aston Feb 03 '20 at 15:55
  • Nice question though! – ParrapbanzZ Feb 03 '20 at 16:03

2 Answers2

1

The issue seems to be the usage of await. When the keyword is encountered, the JavaScript interpreter will immediately pause the rest of the function and only resume the rest of the execution after the Promise is resolved. If you await nonPromiseValue it's treated as if you've created it via Promise.resolve() and is essentially equivalent to

Promise.resolve(nonPromiseValue)
  .then((resolvedValue) => {
    resolvedValue;
  })

Or even more generally this:

await <await expression>;

<rest of body>;

return <return expression>;

Works like this:

Promise.resolve(<await expression>)
  .then(resolvedValue => {
     resolvedValue();

     <rest of body>;

     return <return expression>;
  });

Thus and then you'd still have the rest of the body of the function put on the microtask queue and executed at least the next time the event loop picks a task.

NOTE: I'm omitting some details because the engine would be pausing and unpausing the execution, so if the await expression is part of another statement, you might get slightly different behaviour than what you'd expect at a glance, yet still the reason is immediate pausing when await is encountered. Thus expressions before and after an await keyword would be evaluated at different times and thus might have a different result.

At any rate, here is an example:

async function foo() {
    console.log("foo - start");
    
    "any value without await";
    
    console.log("foo - end")
}

async function bar() {
    console.log("bar - start");
    
    await "any value";
    
    console.log("bar - end")
}

console.log('start');
foo();

bar();
console.log('end');

For foo() the execution is straight forward:

  1. Call the function.
  2. Execute every line in the body.
  3. Finish.

Yes, it's async but it's still executed synchronously. The only difference here is that the result would be a Promise but since it's all synchronous and there is no return keyword, then it's just implicitly Promise.resolve(undefined).

However, for bar() the execution is different:

  1. Call the function.
  2. Execute every line until you encounter await.
  3. Turn the rest of the function to a Promise.
  4. Pause and wait until the Promise is resolved.
    • Since we've await-ed a non-Promise this will happen immediately the next event loop iteration.
  5. Continue the execution.

So, in fact the body of the function is wrapped in a Promise behind the scenes, so we actually run something similar to this:

async function bar() {
    console.log("bar - start");
    
    Promise.resolve("any value") //<-- await "any value";
      .then((resolvedValue) => {
        resolvedValue;            //<-- nothing is done with it but it's what we awaited
        console.log("bar - end"); //<-- the rest of the body of `bar()`
      })
}

console.log('start');
bar();
console.log('end');

This is a simplified view but it's to just help visualise the fact that await will always halt execution and resume later. It uses the same Promise mechanics as usual and would delay the rest of the body for a later iteration of the event loop via a microtask but it's automatically handled for you.

Were we actually await-ing a real Promise, then you'd still get the equivalent behaviour:

async function bazAwait() {
    console.log("bazAwait - start");
    
    const result = await new Promise(resolve => resolve("some Promise value"));
    
    console.log("bazAwait - end", result); //<-- the rest of the body of `bazAwait()`
}

async function bazPromiseEquivalent() {
    console.log("bazPromiseEquivalent - start");
    
    new Promise(resolve => resolve("some Promise value"))//<-- the awaited Promise
    .then((promiseVal) => {                              //<-- the value the Promise resolves with
      const result = promiseVal;                         //<-- the binding for the resolved value
      console.log("bazPromiseEquivalent - end", result); //<-- the rest of the body of `bazPromiseEquivalent()`
    });
}
console.log('start');
bazAwait();
bazPromiseEquivalent();
console.log('end');

I heavily suspect this is done in order to keep the behaviour the same regardless of whether or not await a Promise. Otherwise if you had a line like await myValue you'd get different execution depending on what myValue holds and it will not be obvious until you actually check that.

VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • `Promise.resolve(nonPromiseValue)` is synchronous though IIRC. I think it might rather be a thennable. eg. `Promise.resolve().then(nonPromiseValue)` – Ben Aston Feb 03 '20 at 16:44
  • @Ben the important part is that the rest of the code is executed as a microtask in the `.then`. – VLAZ Feb 03 '20 at 17:34
  • But I think the last sentence of your 1st paragraph needs to change. – Ben Aston Feb 03 '20 at 17:40
  • 1
    @Ben OK, tried to clarify. I don't think it really matters whether you express it through `Promise.resolve(nonPromiseValue)` or `Promise.resolve().then(() => nonPromiseValue)`. In either case it's *the rest* of the body that gets put in the microtask queue. The engine would be doing pausing/unpausing not literally re-writing the function but we can express the same semantics using Promises. So, it's an approximation of what happens behind the scenes. Hopefully close enough to illustrate the behaviour. – VLAZ Feb 03 '20 at 20:04
0

The following code appears to demonstrate how code after an await is treated as a microtask, similar to a then.

A tight indirectly-recursive loop of microtasks is started, that prints the first five integers to the console.

An async function is invoked synchronously. A string inside foo 2 is printed to the console after an await.

I included a generator function to remind us that they are synchronous.

async iterator included for completeness.

Note the interleaving with the "then tasks."

function printNums() {
    let counter = 0
    function go() {
        console.log(counter++)    
        if(counter < 5) Promise.resolve().then(go)
    }
    Promise.resolve().then(go)
}

printNums()

async function foo() {
    console.log('inside foo 1')
    await 1
    console.log('inside foo 2')
}

requestAnimationFrame(() => console.log('raf complete')) // ~16ms
setTimeout(() => console.log('macrotask complete')) // ~4ms

console.log('synch')

const syncIterable = {
    *[Symbol.iterator]() {
        console.log('starting sync iterable')
        yield 'a'
        yield 'b'
    }
}

async function printSyncIterable() {
    for(let y of syncIterable) {
        console.log(y)
    }
}
printSyncIterable()

foo().then(() => console.log('done'))

const asyncIterable = {
    async *[Symbol.asyncIterator]() {
        console.log('starting async iterable')
        yield '⛱'
        yield ''
    }
}

async function printAsyncIterable() {
    for await(let z of asyncIterable) {
        console.log(z)
    }
}
printAsyncIterable()
Ben Aston
  • 53,718
  • 65
  • 205
  • 331