skip to Main Content

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


  1. 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 after await the coded executed as a microtask and the "initializer" is JS itself so the loop() line disappears from the error’s stack.

    You can set a breakpoint and examine the stack in the dev console:

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

    While new Error('').stack doesn’t reflect the microtask, the dev console under Call Stack shows the actual stack’s state:

    enter image description here

    Login or Signup to reply.
  2. 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.

    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. But 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search