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?
2
Answers
JS resolves a promise with a microtask that placed in the end of the current task.
You can read about microtasks in MDN:
https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide
Your particular question about the stack difference is that the first time
loop()
is invoked by you directly in the script so the error’s stack has the invocation line. The further afterawait
the coded executed as a microtask and the "initializer" is JS itself so theloop()
line disappears from the error’s stack.You can set a breakpoint and examine the stack in the dev console:
While
new Error('').stack
doesn’t reflect the microtask, the dev console underCall Stack
shows the actual stack’s state: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 anawait
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 theasync
function, and code that follows that call would have to execute twice (or more) as well. A function only returns once.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.It is the same function — it is resumed. But this time the caller is the event loop, so the call stack starts from scratch. When the
async
function executes areturn
, this return value will be used by the engine to resolve the promise that was returned to the original caller.