Tracking Unhandled Promise Rejections
Ever since jQuery introduced deferreds, developers have used promises to manage asynchronous control flow. (I know, I know, promises pre-date jQuery, but that’s when they went mainstream). In the years since their introduction promises have been standardized and are now being implemented natively in browsers.
Before we can look at error handling in promises, it is useful to see a simple example.
This is a contrived demonstration, but the basic flow is covered here. In this case, we return a promise from the
doSomethingAsync function. The promise returned is “thenable”, meaning it has a
then() function. The callback we pass to
then() is executed once the promise is fulfilled.
What if Something Breaks?
All kinds of things can go wrong in the real world, and the network is never reliable. If an error condition arises inside a promise, you “reject” the promise by calling the
reject() function with an error. To handle a promise rejection, you pass a callback to the
Let’s add some basic error handling to our code:
This is a simple example, so catching the rejection is trivial. As promise chains grow larger, it gets more challenging to reason about the error flow. Additionally, every promise has the potential to fail, so you have two branches you must conceivably handle for every promise.
Promise error handling code can be confusing and clutter the main application logic, so many times we see people elide the
catch() handler altogether. Things work correctly most of the time, right? Ignoring the catch handler might make things easier to read, but what happens if you don’t
catch() a promise rejection?
When a promise is rejected, it looks for a rejection handler. If it finds one, like in the example above, it calls the function with the error. Perfect. If there’s no rejection handler, the promise throws up its hands and produces a global unhandled rejection error.
The problem here is that, until very recently, there was no good way to capture these errors. Unhandled rejections are not sent through normal
console.error(), nor are they passed to the traditional
window.onerror handler. How can we track them?
Modern Standards to the Rescue
The WHATWG html living standard has now been updated with two new window events to help with promise rejection handling. One is for listening to unhandled promise rejections, the other for listening to handled rejections.
Each event handler is passed a PromiseRejectionEvent which contains a
PromiseRejectionEvent.reason property and a
PromiseRejectionEvent.promise property. The former is the actual object the promise was rejected with, and the latter is the promise itself.
These events are not widely supported yet. Chrome is the only browser that has any support natively, and only in version 49 or later. However, it should be noted that Bluebird and some other promise implementations will emit these events in ALL browsers. You can start listening to these events today, and as the spec sees wider implementation, you’ll start getting better data automatically.
Putting it Together With TrackJS
We can add a small bit of code before we use any promises, which will intercept all unhandled and (optionally) handled promise rejections.
If you get in the habit of rejecting your promises with
Error objects instead of strings, TrackJS can also give you the stack trace and other useful debugging information. We highly recommend you do it.
Tracking unhandled promise rejections is still painful, but as the living standard sees broader adoption, promise rejections should be easier to handle. If you’d like help dealing with your unhandled promises, try TrackJS today, free for 14 days, and we’ll help you know when these bugs impact your visitors.