Blog Home » Tracking Unhandled Promise Rejections

Tracking Unhandled Promise Rejections

By Eric Brandes

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.


    function doSomethingAsync(){
        return new Promise(function (resolve, reject){
            setTimeout(function (){
                resolve("success"); // Fulfill the promise successfully
            }, 100);
        });
    }

    doSomethingAsync().then(function (fulfilledResult){
        console.log(fulfilledResult); // Prints "success"
    });

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 catch() function.

Let’s add some basic error handling to our code:


    function doSomethingAsync(){
        return new Promise(function (resolve, reject){
            setTimeout(function (){
                // Note that we reject with an Error object
                // This is useful for getting an accurate stack trace!
                reject(new Error("something went terribly wrong!"));
            }, 100);
        });
    }

    doSomethingAsync()
        .then(function (fulfilledResult){
            // Never executes because the promise is rejected
            console.log(fulfilledResult);
        })
        .catch(function (err){
            console.log(err);  // Prints "Error: something went terribly wrong"
        });

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?

Unhandled Rejections

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.

Unhandled Promise Rejection

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.

WindowEventHandlers.onunhandledrejection

WindowEventHandlers.onrejectionhandled

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.


    window.addEventListener("unhandledrejection", function (event){
        trackJs.track(event.reason);
    });

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.

Eric Brandes
Eric Brandes
CTO and Cofounder