Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get stack trace inside unhandledrejection event handler

How can I determine where the Promise rejection happened when I only caught it using onunhandledrejection handler?

console.error = ()=>{}
window.addEventListener('unhandledrejection', (promiseRejectionEvent) => {
  console.log('unhandled: ', Error().stack)
})

function main() {
  new Promise(() => { throw null })
}
main()

If you check your browser's console after running this, you will see something like:

sources tabconsole output

The Error().stack only includes the rejection handler function itself in its stack trace (grey output js:14:30). But the browser does seem to know where the rejection happened: There is another red error output (Uncaught (in promise) null), pointing to the target line (js:18). How can I access this line information?

It seems that the latter output is being done by the browser's internals, as it is not preventable by overwriting console.error like in the example above. It is only preventable by calling promiseRejectionEvent.preventDefault(), as explained on MDN. But I don't want to prevent it anyway, but retrieve it instead, for example for logging purposes.

Real world use case: It would of course be possible to not rely on onunhandledrejection event handler, e.g. by adding a .catch() phrase or at least throwing throw new Error(null). But in my case, I have no control over it as it is third party code. It threw unexpectedly today (probably a library bug) at a client's browser and the automatic error report did not include a stack trace. I tried to narrow down the underlying issue above. Thanks!


Edit in response to comments:

Wrap the third party code in a try/catch? – weltschmerz

Good point, but this does not help because the rejection actually happens inside a callback:

window.addEventListener('unhandledrejection', (promiseRejectionEvent) => {
  console.log('unhandled: ', Error().stack) // <- stack once again does *not* include "main()", it is only printed out in the console
})

function main() {
  try {
    thirdPartyModule()
  } catch(e) {
    // Never caught
    console.log("caught:", e)
  }
}

// Example code
// We cannot change this function
function thirdPartyModule() {
  setTimeout(() =>
    new Promise(() =>
      { throw null }))
}

main()
like image 223
phil294 Avatar asked Nov 06 '22 06:11

phil294


2 Answers

There is no any good solution to track async stack trace out of the box, but it's possible to do using Zone.js. If you check out the demo on Zone.js page, there is example of an async stack trace.

Zone works by monkey patching all native API's that create async tasks to achieve that.

like image 138
Алексей Мартинкевич Avatar answered Nov 12 '22 13:11

Алексей Мартинкевич


It is not possible.

I imagine the "stack trace" you want would include the line with throw null;, however, that's not in the stack when the unhandledrejection event handler is called. When throw null; is executed, the handler is not called directly (synchronously), but instead a microtask that calls the handler is queued. (For an explanation of the event loop, tasks and microtasks, see "In The Loop" by Jake Archibald.)

This can be tested by queuing a microtask right before throwing the error. If throwing calls the handler synchronously, the microtask should execute after it, but if throwing queues a microtask that calls the handler, the first microtask executes first, and then the second one (that calls the handler).

window.addEventListener('unhandledrejection', (promiseRejectionEvent) => {
  console.log('unhandled: ', Error().stack) // <- stack once again does *not* include "main()", it is only printed out in the console
})

function main() {
  try {
    thirdPartyModule()
  } catch (e) {
    // Never caught
    console.log("caught:", e)
  }
}

// Example code
// We cannot change this function
function thirdPartyModule() {
  setTimeout(() =>
    new Promise(() => {
      Promise.resolve().then(() => { // Queue a microtask before throwing
        console.log("Microtask")
      })
      throw null
    }))
}

main()

As you can see, our microtask executed first, so that means the handler is called inside a microtask. The handler is at the top of the stack.

like image 44
D. Pardal Avatar answered Nov 12 '22 12:11

D. Pardal