Here's an alternative demo that doesn't freeze the browser (which I think is getting the way of what we're trying to show), and which shows the opposite behaviour to your initial conclusion:
const p = new Promise((r) => r());
p.then(() => {
(async() => {
console.log('a');
await p;
console.log('p');
})()
console.log('b');
});
In this case, we get hold of p
, a resolved promise. We then kick off an async function that awaits it. The output is:
a
b
p
IE the async function still returned control back (and b
was logged) when it hit the await
on the already resolved p
. Only after b
was logged and the event loop was free to return execution back to after the await
was p
subsequently logged.
Is this standard behaviour? Yes.
The specification effectively turns the awaited expression to the then
part of a promise, and in steps 9 and 10 of a subsequent step makes the decision on how to proceed.
- If promise.[[PromiseState]] is pending, then
a. Append fulfillReaction as the last element of the List that is promise.[[PromiseFulfillReactions]].
b. Append rejectReaction as the last element of the List that is promise.[PromiseRejectReactions]].
- Else if promise.[[PromiseState]] is fulfilled,
a. then Let value be promise.[[PromiseResult]].
b. Let fulfillJob be NewPromiseReactionJob(fulfillReaction, value)
c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]).
Step 9 says if it's pending, to add the generated then
to the list of things to be called when the promise is fulfilled.
Step 10 gets to the heart of your question - it says if it's fulfilled already, to enqueue the job - ie to place it on the queue of things to be called back.
The spec effectively says that an await
should never return synchronously.