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:
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()
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With