When I was reading spec I saw next part:
- NOTE: Copying the execution state is required for AsyncBlockStart to resume its execution. It is ill-defined to resume a currently executing context.
I don't understand this. Why do we need to copy execution context? Can't we do it without extra execution context or what will be broken without copying in that case?
Evaluation of an async function body happens in a separate execution context that can be repeatedly resumed and suspended. The algorithm steps executed in this context are given in AsyncBlockStart #3.
On await
(in Await #8) and completion (i.e. return
/throw
, in AsyncBlockStart #3.g), the execution context is popped off the stack (and in case of await
, suspended to resume where it left off, in Await #9).
On promise fulfillment/rejection (in Await #3.c/5.c) and when starting the async function (in AsyncBlockStart #4), it is pushed onto the stack and resumed.
These push/pop operations need to symmetrically correspond to each other, both when starting and resuming the code it may run into either a suspension or the end of the code; and in all four cases the stack must have the same running execution context on top before and after.
In case of a resumption from promise settlement, that running execution context will be the current promise job. In case of AsyncFunctionStart, that running execution context will be the one created and pushed by the PrepareForOrdinaryCall steps during the [[Call]] to the async function (which goes through OrdinaryCallEvaluateBody, EvaluateBody to EvaluateAsyncFunctionBody which creates the promise and performs AsyncFunctionStart). It will afterwards be popped from the stack in [[Call]] #7 like for any other function.
So why do we need an extra execution context? Because if we didn't create a new one (as a copy of the current), it would have been popped off already when AsyncFunctionStart ends, and [[Call]] would fail to pop it again. (Or worse, pop one too many). Of course, an alternative solution to this problem would have been to not make a copy of the current execution context, reuse the suspendable execution context instead, and just push it again onto the stack (without resuming it, only setting it as the running execution context) after the AsyncBlockStart in AsyncFunctionStart #4. But that would've been weird, no?
After all, it doesn't matter which way it's specified, the outcome would be the same. The execution context is not observable from user code.
Note: re-using the same execution context is in fact what generators do. GeneratorStart #2 (which is called from EvaluateGeneratorBody, where the parameter declarations are evaluated and the Generator instance is created) does use the running execution context as the genContext
that is repeatedly resumed and suspended. The main difference is that the start ("first resumption") doesn't already happen during the function call for generators (as it does happen for async functions), it will only happen later in the first next()
call.
And actually "It is ill-defined to resume a currently executing context." doesn't apply here. The currently executing context would get implicitly suspended in AsyncBlockStart #3 by setting "the code evaluation state of asyncContext
such that when evaluation is resumed […]", just like it does happen in GeneratorStart #4.