javascriptes6-promisev8internalsjavascriptcore

What happens with async await exactly?


I have such a piece of code

async function loop() {
  for (let i = 0; i < 3; i++) {
    console.log(i,new Error("").stack);
    await 1;
  }
}

loop();

when I run it in Node (Chrome engine) I get this:

0 Error
    at loop (file:///Users/user/Desktop/test.mjs:3:19)
    at file:///Users/user/Desktop/test.mjs:8:1
    at ModuleJob.run (node:internal/modules/esm/module_job:217:25)
    at async ModuleLoader.import (node:internal/modules/esm/loader:308:24)
    at async loadESM (node:internal/process/esm_loader:42:7)
    at async handleMainPromise (node:internal/modules/run_main:66:12)
1 Error
    at loop (file:///Users/user/Desktop/test.mjs:3:19)
2 Error
    at loop (file:///Users/user/Desktop/test.mjs:3:19)

so it seems that after await the execution loses its broader context but retains the context of the function.

When I run the same piece of code in Bun (Safari engine) I get this:

0 Error: 
    at <anonymous> (/Users/user/Desktop/test.mjs:3:10)
    at loop (/Users/user/Desktop/test.mjs:1:22)
    at module code (/Users/user/Desktop/test.mjs:5:5)
1 Error: 
    at <anonymous> (/Users/user/Desktop/test.mjs:3:10)
2 Error: 
    at <anonymous> (/Users/user/Desktop/test.mjs:3:10)

which says that the execution loses even the context of the function.

Now, I know what happens when I use await like this, more or less. It forces stuff to be pushed to the micro task queue, lets the rest of the sync code on the stack execute, therefore we lose the stack, and picks up the stuff pushed to the queue afterwards.

However, I'm interested in how exactly this happens and looking at these different errors stacks I'm really confused. Are we in the same function after using await or is a new function created somehow with the context of the previous one?


Solution

  • the execution loses even the context of the function.

    I assume with "context" you refer to the execution contexts of the callers of the function. These contexts have been closed normally by their synchronous execution, while the execution context of the function itself is suspended.

    An important realisation here is that the async function returns when it has evaluated an await expression. It returns a promise, and the call stack (what you call the "context") will run to completion. The call stack is empty, and will not be restored. Restoring it would make no sense, as it would indicate that a caller would get twice (or more) a return value from the async function, and code that follows that call would have to execute twice (or more) as well. A function only returns once to its caller.

    So once the microtask is consumed, the execution context of the async function will be restored. But this does not include a restored call stack.

    Are we in the same function after using await or is a new function created somehow with the context of the previous one?

    It is the same function -- it is resumed. See this as a new call of the function, but where it starts with a saved execution context, and continues from there. This time the caller is the event loop, so the call stack starts from scratch. When the async function executes a return, this return value will be used by the engine to resolve the promise that was returned to the original caller.