Catch Errors in JavaScript Promise Chains

Understanding how to catch errors in promise chains or even when to use error catching with promises can be confusing. Furthermore, there are a number of ways to catch and throw errors with libraries like Q. However, if you implement error handling appropriately it can be helpful in debugging and in providing informative error messages to users.

A hypothetical example:

function fetchData() {
    return Q([1, 2, 3]);
}

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('handling ' + data);
            return data.reduce(function (memo, val) { return memo + val; });
        }, function (error) {
            console.error('handle error: ' + error.stack);
            throw error;
        });
}
 
function useData() {
    console.log('start');
    return handleData()
        .then(function (result) {
            console.log('using ' + result);
        }, function (error) {
            console.error('use error: ' + error.stack);
        })
        .fin(function () {
            console.log('finish');
        });
}
 
useData();
// start
// handling 1,2,3
// using 6
// finish

JS Fiddle

In this example fetchData simply returns a fulfilled promise, but pretend it’s a function that asynchronously fetches data. Upon successfully fetching the data, the function would fulfill the promise with the data. Alternatively, fetchData could resolve with a rejected promise. handleData and useData simply represent consuming functions since nesting functions that return promises can be a common way to breakdown functionality. The calls to console.log and console.error represent whatever “work” is done after the promise is fulfilled or rejected which may include error logging and/or the error messages displayed to users in the event of unexpected or expected errors (e.g. invalid user input). The error handling in this example is purely demonstrative and doesn’t represent best practice.

Now let’s introduce errors into the example and show possible ways to handle them.

Instead of fulfilling the original promise, I’m going to modify fetchData to return a rejected promise with an error.

function fetchData() {
    return Q.reject(Error('401 Unauthorized'));
}

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('handling ' + data);
            return data.reduce(function (memo, val) { return memo + val; });
        }, function (error) {
            console.error('handle error: ' + error.stack);
            throw error;
        });
}
 
function useData() {
    console.log('start');
    return handleData()
        .then(function (result) {
            console.log('using ' + result);
        }, function (error) {
            console.error('use error: ' + error.stack);
        })
        .fin(function () {
            console.log('finish');
        });
}
 
useData();
// start
// handle error
// use error
// finish

JS Fiddle

In the Q API reference, the first and second parameters of promise.then are known as onFulfilled and onRejected respectively. If you run the example above with the modified fetchData function you’ll notice that the onRejected function is called in each then method within handleData and useData.

It is important to notice that the onRejected function in handleData does not simply return the error, but throws the error. To understand what’s really going on here it’s important to understand that when then is executed it creates a new promise. To explain it using code:

promise2 = promise1.then(onFulfilled, onRejected);

If you compare this snippet to the example above, promise1 represents the promise returned by fetchData. promise2 represents the promise returned by then in handleData and is the same promise on which then is executed in useData. A common point of confusion and incorrect assumption is that handleData is returning the promise returned by fetchData, which is NOT what happens.

promise2 is either fulfilled or rejected based on what happens in onFulfilled and onRejected. If you want an error to be caught by a subsequent onRejected function then you must throw the error and not return it. Throwing the error essentially rejects the promise created by then (promise2). In other words, when then is executed it creates a new promise, which is fulfilled if no error is thrown or rejected if you throw an error in onFulfilled or onRejected.

In the event you don’t want to throw the error, you could handle it and return a rejected promise so that any subsequent onRejected functions are called. Additionally, you could handle the error and return nothing so that the promise resolves and any subsequent onFulfilled functions are called.

The following example demonstrates what happens when you return an error:

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('handling ' + data);
            return data.reduce(function (memo, val) { return memo + val; });
        }, function (error) {
            console.error('handle error: ' + error.stack);
            return error;
        });
}
 
useData(); 
// start
// handle error
// using Error: 401 Unauthorized
// finish

JS Fiddle

The onFulfilled function is called in useData because the error is returned. The intention of the code is not to use the error, thus it should throw it or return something useable instead.

What if we needed to fetch other data based on the original data we received? Our data fetching and handling functions might look something like this:

function fetchData() {
    return Q([1, 2, 3]);
}

function fetchOtherData(params) {
    return Q(params.map(function (val) { return val * 2; }))
        .then(function (result) {
            console.log('using ' + result);
        }, function (error) {
            console.error('use error: ' + error.stack);
        });
}

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('fetching other data with ' + data);
            return fetchOtherData(data);
        }, function (error) {
            console.error('handle error: ' + error.stack);
            return Q.reject();
        });
}
 
function useData() {
    console.log('start');
    return handleData()
        .fin(function () {
            console.log('finish');
        });
}

useData(); 
// start
// fetching other data with 1,2,3
// using 2,4,6
// finish

JS Fiddle

With this example, don’t focus on what is happening to the data but simply the idea that something else asynchronous is happening that requires a promise. It just so happens I decided to “fetch more data” based on the initial dataset because I’ve seen this happen in real applications. Also, I moved some of the data handling to fetchOtherData.

Now I’m going to start making some changes. Let’s say that I misspell the fetchOtherData function and call it fetchMoreData, which is an undefined function. What happens?

function fetchData() {
    return Q([1, 2, 3]);
}

function fetchOtherData(params) {
    return Q(params.map(function (val) { return val * 2; }))
        .then(function (result) {
            console.log('using ' + result);
        }, function (error) {
            console.error('use error: ' + error.stack);
        });
}

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('fetching other data with ' + data);
            return fetchMoreData(data);
        }, function (error) {
            console.error('handle error: ' + error.stack);
            return Q.reject();
        });
}
 
function useData() {
    console.log('start');
    return handleData()
        .fin(function () {
            console.log('finish');
        });
}

useData();
// start
// fetching other data with 1,2,3
// finish

JS Fiddle

You may have expected the error handler onRejected in handleData to catch the exception that was thrown from calling the undefined function fetchMoreData. However, that is not the case. The onRejected function in handleData only handles errors or rejected promises returned from the preceding promise, in this case, the promise returned by fetchData. An onRejected function will NOT handle a rejected promise or error thrown in a neighboring onFulfilled function, (i.e. the onFulfilled function that is passed as an argument to the same then that onRejected is passed to).

You also may be surprised to notice that the console reports no errors whatsoever. Since we didn’t provide adequate error handling in this scenario, the exception was “swallowed” in our promise chain. As you can imagine, this would make debugging pretty tricky and frustrating. There are a number of different things we could do to correct this.

You could add a call to fail (also known as catch) in your promise chain. fail is simply syntactic sugar for then(null, onRejected).

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('fetching other data with ' + data);
            return fetchMoreData(data);
        }, function (error) {
            console.error('handle error: ' + error.stack);
            return Q.reject();
        })
        .fail(function (error) {
            console.error(error.stack);
        });
}

useData(); 
// start
// fetching other data with 1,2,3 
// ReferenceError: fetchMoreData is not defined
// finish

JS Fiddle
The onRejected function passed to fail receives the return value of the rejected promise returned by then, which in this case is a ReferenceError exception.

Additionally, you could remove the onRejected passed to then fail so as not to have two onRejected functions that essentially handle errors the same way. However, depending on your situation you may want separate onRejected functions to handle the rejections differently.

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('fetching other data with ' + data);
            return fetchMoreData(data);
        })
        .fail(function (error) {
            console.error(error.stack);
        });
}

useData(); 
// start
// fetching other data with 1,2,3 
// ReferenceError: fetchMoreData is not defined
// finish

JS Fiddle

This newly added call to fail will handle rejections from the preceding then and any rejections from fetchData.

function fetchData() {
    return Q.reject(Error('BAZINGA!'));
}

function fetchOtherData(params) {
    return Q(params.map(function (val) { return val * 2; }))
        .then(function (result) {
            console.log('using ' + result);
        }, function (error) {
            console.error('use error: ' + error.stack);
        });
}

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('fetching other data with ' + data);
            return fetchMoreData(data);
        })
        .fail(function (error) {
            console.error(error.stack);
        });
}
 
function useData() {
    console.log('start');
    return handleData()
        .fin(function () {
            console.log('finish');
        });
}

useData();
// start
// ReferenceError: BAZINGA!
// finish

JS Fiddle

As a final example, it’s usually a good idea to add done to the end of promise chains because it won’t swallow nasty errors the way then does.

function fetchData() {
    return Q([1,2,3]);
}

function fetchOtherData(params) {
    return Q(params.map(function (val) { return val * 2; }))
        .then(function (result) {
            console.log('using ' + result);
        }, function (error) {
            console.error('use error: ' + error.stack);
        });
}

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('fetching other data with ' + data);
            return fetchMoreData(data);
        }, function (error) {
            console.error('handle error: ' + error.stack);
            return Q.reject();
        });
}
 
function useData() {
    console.log('start');
    handleData()
        .done(function () {
            console.log('finish');
        });
}

useData();
// start
// fetching other data with 1,2,3 
// Uncaught ReferenceError: fetchMoreData is not defined 

JS Fiddle

Notice that done “handles” the error without an onRejected error handler. In reality, it doesn’t handle the error but allows it to be raised to window.onerror.

The difference between then and done can be confusing. The Q API provides a great rule of thumb:

The Golden Rule of done vs. then usage is: either return your promise to someone else, or if the chain ends with you, call done to terminate it. Terminating with catch is not sufficient because the catch handler may itself throw an error.

This is because “exceptions thrown in then callbacks are consumed and transformed into rejections, [so] exceptions at the end of the chain are easy to accidentally, silently ignore” where as with done “the resulting rejection reason is thrown as an exception in a future turn of the event loop” (Q API). done makes sure the error is not caught and not ignored so that some kind of error is raised.

Also, I could have provided an onRejected function to done and handled the error some other way, but in this example I wanted to illustrate that done wouldn’t swallow the error like then.

As an interesting side note, if you were to store the promise that was swallowed in the original example above and inspect it, you would notice that it has been rejected. If you call fail or done on that promise, you can handle the rejection like so:

function fetchData() {
    return Q([1,2,3]);
}

function fetchOtherData(params) {
    return Q(params.map(function (val) { return val * 2; }))
        .then(function (result) {
            console.log('using ' + result);
        }, function (error) {
            console.error('use error: ' + error.stack);
        });
}

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('fetching other data with ' + data);
            return fetchMoreData(data);
        }, function (error) {
            console.error('handle error: ' + error.stack);
            return Q.reject();
        });
}
 
function useData() {
    console.log('start');
    return handleData()
        .fin(function () {
            console.log('finish');
        });
}

var prom = useData();
Q(prom).fail(function () {
    console.log(prom.isRejected());
    console.log(prom.inspect());
});
// start
// fetching other data with 1,2,3
// finish 
// true
// Object {state: "rejected", reason: ReferenceError}

JS Fiddle

All said and done (pun intended), there is one final question I want to address: What if an error is thrown in done?

You may have noticed I already answered this when quoting the Q API, but I would like to provide an example.

function fetchData() {
    return Q([1,2,3]);
}

function fetchOtherData(params) {
    return Q(params.map(function (val) { return val * 2; }))
        .then(function (result) {
            console.log('using ' + result);
        }, function (error) {
            console.error('use error: ' + error.stack);
        });
}

function handleData() {
    return fetchData()
        .then(function (data) {
            console.log('fetching other data with ' + data);
            return fetchOtherData(data);
        }, function (error) {
            console.error('handle error: ' + error.stack);
            return Q.reject();
        });
}
 
function useData() {
    console.log('start');
    handleData()
        .done(function () {
            throw Error('oops!');
            console.log('finish');
        }, function (ex) {
            console.error(ex.stack);
        });
}

useData();
// start
// fetching other data with 1,2,3
// using 2,4,6
// Uncaught Error: oops! 

JS Fiddle

Even when an error is thrown in the onFulfilled function passed to done, the error will still be raised. Similar to then, the onRejected function I provided to done does not handle the error thrown in onFulfilled. It is simply thrown as an Uncaught Error.

Now that the promise chain has been terminated with done, if you were to continue the promise chain you would get an error because nothing is returned.

function useData() {
    console.log('start');
    return handleData()
        .done(function () {
            console.log('finish');
        }, function (ex) {
            console.error(ex.stack);
        });
}

useData().then(function () {}, function () {});
// Uncaught TypeError: Cannot read property 'then' of undefined
// fetching other data with 1,2,3
// using 2,4,6
// finish

JS Fiddle

2 Comments

  1. Great explaination of the difference between throwing the error and returning it. Very helpful.

    Reply

  2. Great post, thanks for sharing.

    Question (I am new promises): What happens when a function before returning has an error, how does that error gets handled?

    Lets say in your example code:

    function handleData() {
    var x = fs.notFuncCall(); // What if the error happens here
    return fetchData()
    .then(function (data) {
    console.log(‘fetching other data with ‘ + data);
    return fetchOtherData(data);
    }, function (error) {
    console.error(‘handle error: ‘ + error.stack);
    return Q.reject();
    });
    }

    How would this error gets caught in chain of promises?

    Reply

Comments, questions and feedback welcome.