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()
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 ments:
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()
Share
Improve this question
edited Aug 2, 2020 at 16:34
phil294
asked Jul 6, 2020 at 23:36
phil294phil294
10.9k8 gold badges72 silver badges107 bronze badges
2
- Wrap the third party code in a try/catch? – Dennis Hackethal Commented Aug 2, 2020 at 4:40
- @weltschmerz Yes, but the third party code does not actually return a Promise, so it cant be caught. I updated the answer with an example. Thanks – phil294 Commented Aug 2, 2020 at 16:36
4 Answers
Reset to default 5Disclaimer: Using this workaround is useful for debugging, but has minor negative performance implications, and could cause various libraries to misbehave; it should not be used in production code.
You can replace the Promise constructor with your own implementation that includes a stack trace for where it was created.
window.Promise = class FAKEPROMISE extends Promise {
constructor() {
super(...arguments);
this.__creationPoint = new Error().stack;
}
};
This will give you a stack trace of the point where any given promise was created.
Note that .then
, .catch
, and .finally
all create new promises.
This will not function with Promises created by async
functions, as those do not use the window's Promise
constructor.
This can be used by reading the promise
member of the PromiseRejectionEvent
:
window.addEventListener('unhandledrejection', (promiseRejectionEvent) => {
console.log('unhandled: ', promiseRejectionEvent.promise.__creationPoint)
})
Which will print something like:
unhandled: Error
at new FAKEPROMISE (script.js:4:32)
at main (script.js:6:3)
at script.js:9:1
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.
process.on('unhandledRejection', (reason, promise) => {
console.log('stackTrace:', reason.stack);
});
"reason" is a native Error object. You can display the stackTrace with the "stack" property.
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.