Background
This code can leave the system in the inconsistent state:
1 | var q = require('q'); |
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.