Error handling in promises

Handling the errors in the promise chain.

Background

This code can leave the system in the inconsistent state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var q = require('q');
function foo() {
var defer = q.defer();
process.nextTick(function () {
defer.resolve();
});
return defer.promise;
}
var state = 'before foo';
foo()
.then(function onSuccess() {
console.log('success!');
state = 'foo finished';
}, function onError() {
console.log('error :(');
});
process.on('exit', function () {
console.log('on exit, state =', state);
});

If everything goes well, the program prints (I am using node):

$ node index.js
success!
on exit, state = foo finished

two problems

The obvious problem: if foo throws an error, or rejects the promise, the onError callback never sets the correct state.

var q = require('q');
function foo() {
    var defer = q.defer();
    process.nextTick(function () {
        defer.reject();
    });
    return defer.promise;
}
var state = 'before foo';
foo()
.then(function onSuccess() {
    console.log('success!');
    state = 'foo finished';
}, function onError() {
    console.log('error :(');
});
process.on('exit', function () {
    console.log('on exit, state =', state);
});
// prints
error :(
on exit, state = before foo

Less obvious problem: if onSuccess throws an error, the function onError is NOT called

var q = require('q');
function foo() {
    var defer = q.defer();
    process.nextTick(function () {
        defer.resolve();
    });
    return defer.promise;
}
var state = 'before foo';
foo()
.then(function onSuccess() {
    console.log('success!');
    throw new Error('unexpected error');
    state = 'foo finished';
}, function onError() {
    console.log('error :(');
});
process.on('exit', function () {
    console.log('on exit, state =', state);
});
// prints
success!
on exit, state = before foo

Lets split this problem into two components: setting the right final state and handling all errors.

Setting the final state

Q and its q-lite version used by AngularJs allow setting a callback to execute in any situation, whether the promise is resolved or rejected via .finally.

var q = require('q');
function foo() {
    var defer = q.defer();
    process.nextTick(function () {
        defer.reject();
    });
    return defer.promise;
}
var state = 'before foo';
foo()
.then(function onSuccess() {
    console.log('success!');
    throw new Error('unexpected error');
}, function onError() {
    console.log('error :(');
})
.finally(function setState() {
    state = 'foo finished';
});
process.on('exit', function () {
    console.log('on exit, state =', state);
});
// prints
error :(
on exit, state = foo finished

Always handling errors

You can use .then(onSuccess, onError) syntax to handle errors happening before this step. To handle all errors, including inside the onSuccess callback, you can set the error handler at the end of the promise chain

var q = require('q');
function foo() {
    var defer = q.defer();
    process.nextTick(function () {
        defer.resolve();
    });
    return defer.promise;
}
var state = 'before foo';
foo()
.then(function onSuccess() {
    console.log('success!');
    throw new Error('unexpected error');
})
.fail(function onError() {
    console.log('error :(');
})
.finally(function setState() {
    state = 'foo finished';
});
process.on('exit', function () {
    console.log('on exit, state =', state);
});
// prints
success!
error :(
on exit, state = foo finished

bonus: .done

What happens if an error is thrown and you do not have a final .fail callback to handle it? The promise chain silently hides the exception, as if nothing happened. Here is an example:

var q = require('q');
function foo() {
    var defer = q.defer();
    process.nextTick(function () {
        defer.resolve();
    });
    return defer.promise;
}
var state = 'before foo';
foo()
.then(function onSuccess() {
    console.log('success!');
    throw new Error('unexpected error'); // 1
})
.finally(function setState() {
    state = 'foo finished';
});
process.on('exit', function () {
    console.log('on exit, state =', state);
});
// prints
success!
on exit, state = foo finished

What has happened to the error thrown in // 1? It has simply disappeared.

To make sure any unhandled error inside the promise chain gets rethrown to the outside environment, please close the chain using .done call.

var q = require('q');
function foo() {
    var defer = q.defer();
    process.nextTick(function () {
        defer.resolve();
    });
    return defer.promise;
}
var state = 'before foo';
foo()
.then(function onSuccess() {
    console.log('success!');
    throw new Error('unexpected error');
})
.finally(function setState() {
    state = 'foo finished';
})
.done();
process.on('exit', function () {
    console.log('on exit, state =', state);
});
// prints
success!
on exit, state = foo finished
    /error-handling-promises/node_modules/q/q.js:126
        throw e;
Error: unexpected error
    at onSuccess (/error-handling-promises/index.js:13:8)

If you think no one else needs to attach to your promise chain, use .done(). If your method does return the promise, please still use .fail to handle all errors in your function.

Conclusion

The world is brittle, and the system has to prepare for errors that WILL happen. By using .error and .finally in your promise chains, you can catch all errors and leave the system in a consistent state.