Why await outperforms promise in JS asynchronous stack tracing



The fundamental difference between async/await and promise is that await fn() suspends the execution of the current function, while promise Then (FN) continues to execute the current function after adding the FN call to the callback chain.

const fn = () => console.log('hello')
const a = async () => {
  Await fn() // pause FN execution
//The execution of FN is resumed only when a is called
a() // "hello"

const promise = Promise.resolve()
//After adding FN to the callback chain, continue to execute FN
promise.then(fn) // "hello"

In the context of stack tracing, this difference is very significant.

When a promise chain (whether deglycosylated or not) throws an unhandled exception at any time, the JavaScript engine will display an error message and (hopefully) record a useful stack trace.

As a developer, whether you are using a normal promise or async await, you will expect this.


Imagine a scenario. When the call to asynchronous function B is resolved, call function C:

const b = () => Promise.resolve()
const a = () => {
    b().then(() => c())

When a is called, the following occurs synchronously:

  • B is called and returns a promise, which will be solved at some time in the future.
  • . Then callbacks (actually calls C ()) are added to the callback chain (in V8 terminology, […] is added as a parsing handler).

After that, we finished executing the code in the body of function a. A will never be suspended. When the asynchronous call to B is resolved, the context has disappeared.

Imagine what would happen if B (or C) threw an exception asynchronously? Ideally, the stack trace should include a, because B (or C) is called from there, right? Since we are no longer referring to a, how can we do that?

To make it work, the JavaScript engine needs to do something beyond the above steps: it captures and stores stack traces when it has the opportunity.

In V8, the stack trace is attached to the promise returned by B. When promise is implemented, the stack trace will be passed so that C can use it as needed.

b()[a] -> b().then()[a] -> c[a?:a]

Capturing stack traces takes time (i.e., reduces performance); Memory is required to store these stack traces.


The following is the same program, written in async/await instead of promise:

const b = () => Promise.resolve()
const a = async () => {
  await b()

With await, we can recover the call chain even if we do not collect stack traces in await calls.

This is possible because a is suspended and waiting for B to resolve. If B throws an exception, the stack trace can be rebuilt in this way as needed.

If C throws an exception, the stack trace can be constructed like a synchronous function, because when this happens, we are still in the context of A.

Enable the JavaScript engine to handle stack tracing in a more efficient manner by following these recommendations:

  • Prefer async/await over promise.
  • Use @babel/preset env to avoid unnecessary async/await transfers.

The above is the details of why await is better than promise in JS asynchronous stack tracing. For more information about JavaScript, please pay attention to other developeppaer related articles!