Logging And Debugging Unhandled Promise Rejections In Node.js v1.4.1 And Later
This morning, I was reading through the Node.js "process" documentation when I noticed that you could bind an event handler for "unhandled rejections". This event would be for Promise chains that are resolved in a rejection value but have no .catch() handler to address said rejection. This is really cool and I can't believe I didn't know about it. As such, I wanted to give it a quick try as well help spread the word on this feature (assuming that I'm not the only unaware one in the group).
In a vacuum, there's nothing technically wrong with having a Promise chain result in an unhandled rejection. But, when done in the context of a production JavaScript application, having an unhandled rejection can lead to mysterious and hard-to-debug behavior since meaningful errors may disappear into the nothingness. This can leave your users with a poor experience and leave the engineers with very little to debug.
Yes, there are use-cases where you don't really care about Promise errors. For example, a logging or heart-beat API might be seen as a "send and forget" kind of call:
heartbeat.ping()
But, for the most part - in my experience - if a Promise chain doesn't explicitly handle rejections, it is a bug that should be fixed. And, now with the "unhandledRejection" process event, we can locate these problematic Promise chains, log the point of error, and fix them in a subsequent deployment.
To see this in action, I've put together a simple demo that registers an "unhandledRejection" event handler and initiates a Promise chain with no rejection handler:
// Require our core node modules.
var chalk = require( "chalk" );
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
// Added in Node.js v1.4.1, this is a global event handler that will be notified of
// Promise values that do not have a .catch() handler (or some kind) attached to them.
// --
// NOTE: Some 3rd-party Promise libraries like Bluebird and Q will catch unhandled
// rejections and emit an "unhandledRejection" event on the global process object.
process.on(
"unhandledRejection",
function handleWarning( reason, promise ) {
console.log( chalk.red.bold( "[PROCESS] Unhandled Promise Rejection" ) );
console.log( chalk.red.bold( "- - - - - - - - - - - - - - - - - - -" ) );
console.log( reason );
console.log( chalk.red.bold( "- -" ) );
}
);
// Now, let's create a Promise chain that has no error handling.
var promise = Promise
.resolve( "I can haz fulfillment?!" )
.then(
function( value ) {
throw( new Error( "Something went wrong." ) );
}
)
;
As you can see, we're throwing an Error in our Promise chain and providing no .catch() handler. And, when we run this through Node.js, we get the following terminal output:
As you can see, Node.js detected the unhandled Promise rejection and passed both the unhandled error and the contextual promise to our "unhandledRejection" event handler. Very cool!
I should also mention that popular Promise libraries like Q and Bluebird will detect unhandled rejections in their own Promise implementations and hook into this Node.js process event (if process is an object in the current context). This means that you should be able to globally detect unhandled Promise rejections whether you're using the native Promise module or one of the popular community-driven Promise implementations.
As a final note, I wanted to go back to the "send and forget" heartbeat example from above. You can still use the "unhandledRejection" process event handler in conjunction with "send and forget" actions - you just need to add a no-op (no operation) catch-handler:
heartbeat.ping().catch(
function( error ) {
// The heart-beat is a "send and forget" operation. We don't care if it
// fails; and, even if it does fail, there's nothing meaningful we can
// do about it in the context of this workflow.
}
);
Not only does this satisfy the need for a catch-handler, it also makes the code much more obvious to the next engineer that comes along. It clearly signals that an error may occur; and, that if it does, ignoring the error is an explicit and intentful part of the business logic. This leaves no room for ambiguity.
I have personally helped other engineers try to locate ignored Promise rejections that were causing bugs in our application. It is quite a frustrating task because, with little to go on, you literally have to step through the request line-by-line trying to find possible error cases. Having this global "unhandledRejection" event-handler is going to make life so much easier to debug!
Want to use code from this post? Check out the license.
Reader Comments
Ben - I'm pretty sure that should be `unhandledrejection` all lower case (at least that's the examples I'm working against with React/in the browser... maybe it's different in node?
@Brian,
You are correct, it is different in Node.js and the Browser. Node.js uses "unhandledRejection" and the Browser uses "unhandledrejection". You can see the two cases in the Bluebird documentation:
http://bluebirdjs.com/docs/api/error-management-configuration.html#global-rejection-events
I'm playing around with the Browser-based stuff right now.
@All,
I did a fun little follow-up on this concept in the Browser:
www.bennadel.com/blog/3239-logging-and-debugging-unhandled-promise-rejections-in-the-browser.htm
Natively, Chrome is the only browser that supports the (lowercase R) "unhandledrejection" event; however, we can use Bluebird and Q to shim this behavior into other browsers.