This simple snippet of Javascript has saved me untold amounts of headache and heartache since I first started adding it to my Node.js projects:
process.on('unhandledRejection', err => {
console.log('Unhandled rejection:', err);
});
Let’s dig into the sorry state of unhandled promise rejections in Node.js and find out why this simple piece of code can be such a life-saver in sufficiently large projects.
An Unresolved Promise
Imagine we have the following code buried deep within our Node.js application:
const foo = () => get('foo').then(res => res.body);
const bar = () => get('bar').then(res => res.body);
Our foo
function makes a call to the asynchronous get
function, which returns a Promise
. After the promise resolves, we return the result’s body
. Our bar
function makes a similar call to get
and also returns the result’s body
.
Running our application results in the following incredibly unhelpful error message:
(node:5175) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: Cannot read property 'body' of undefined
(node:5175) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
So somewhere in my codebase, the body
field is trying to be accessed on undefined
. Where? Who knows. What’s the context? No telling! How do we track it down? I don’t know, maybe a Ouija board?
Tools for Handling Rejection
Thankfully, Node.js ships with the tools required to remedy this situation. Much like the more well-known "uncaughtException"
process event, Node.js applications can listen for "unhandledRejection"
events at the process level. These events are fired any time a rejection bubbles to the top of a promise chain without encountering a catch
callback.
Let’s add an "unhandledRejection"
listener to our application. We’ll keep things simple and log the error reported by the process:
process.on('unhandledRejection', err => {
console.log('Unhandled rejection:', err);
});
Let’s try running our application again:
Unhandled rejection: TypeError: Cannot read property 'body' of undefined
at get.then.res (/Users/pcorey/test/promise.js:11:46)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
at Function.Module.runMain (module.js:678:11)
at startup (bootstrap_node.js:187:16)
at bootstrap_node.js:608:3
Sweet clarity!
Our "unhandledRejection "
event listener is report on our unhandled rejection, and now we’re given a stack trace that pinpoints the source of the error. We can clearly see that the unhandled rejection is occurring on line 11
of our example application:
const bar = () => get('bar').then(res => res.body);
It looks like our asynchronous call to get('bar')
is returning undefined
, and our res.body
expression is throwing an exception. Given this information, we can easily debug the situation and come up with a solution.
The Future Can’t Come Soon Enough
As mentioned in our earlier error:
In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
As far as I’m concerned, this is the expected and preferred behavior. Keeping an application alive after an unhandled exception has bubbled up to the event loop, even within the context of a promise, should result in the process being killed and a proper stack trace being logged.
The future can’t come soon enough.