Understanding exceptions in Promises
Many articles exist on how promises work. This article attempts to explain error propgation when using promises.
Lets start with a simple function that throws an exception.
function boom() {
throw 'BOOM!';
}
What happens when we execute the boom
function as a promise using Q's fcall
method?
Q.fcall(boom);
A big ole heap of nothing! We see no output, no exception, no stacktrace. Nothing. The exception gets 'swallowed' by Q.
It seemingly disappears because a promise has one of two outcomes: fulfilled or rejected.
An exception causes the promise to be rejected. So in order to see the reason for the rejection, we must implement a rejection handler. Q provides several techniques for this.
fail
The simplest way to handle an exception is to use the fail
method. The code below will output the error to the console.
Q.fcall(boom)
.fail(function(err) {
console.log(err);
});
Nice and simple eh? fail
is actually just syntatic sugar for the rejection handler of then
.
then
Typically, we handle chaining promises via then
. This function takes two arguments: fulfilled handler and rejected handler. It returns a promise based on the execution of one of these handlers.
Q.fcall(boom)
.then(
// fulfill handler
function() { },
// reject handler
function(err) {
console.log(err);
}
);
One of these two functions will get executed depending on the prior promise's result.
In our example, since we have an exception, it executes the rejection handler and would output the error to the console.
This is pretty straight forward, but there is one gotcha that tends to trip people up. What happens if a handler throws an exception?
Q.fcall(function() {
return 1;
})
.then(
function(number) {
throw 'Oh no!';
},
function(err) {
console.log('I will never get hit!');
}
);
In the above example, the first promise is fulfilled. The subsequent fulfillment handler then throws the exception. You might think that the corresponding rejection handler would execute, but you would be wrong.
The handlers here are responding to the prior promise, our original one. Only one of the two methods will execute, never both.
If the prior promise is fulfilled, then
uses the fulfillment handler.
If the prior promise is rejected, then
uses the rejection handler.
Equally important is that then
constructs a new promise with the chosen handler.
So we're back to square one... we have a new promise that was rejected and we need a way to handle it. We have to continue down the chain...
Q.fcall(function() {
return 1;
})
.then(function(number) {
throw 'Oh no!';
})
.then(
function() { },
function(err) {
console.log('This... is getting old');
}
);
One other thing to note is that Q will skip any then
statements that don't implement a rejection handler until one is found.
Q.fcall(function() {
throw 'Oh no!';
})
.then(function() {
// I get skipped
})
.then(function() {
// I get skipped too
})
.then(
function() { }
function(err) {
console.log('I am handled');
}
);
Or alternatively using a fail
syntatic sugar...
Q.fcall(function() {
throw 'Oh no!';
})
.then(function() {
// I get skipped
})
.then(function() {
// I get skipped too
})
.fail(function(err) {
console.log('I am handled');
});
Summary
There are a few things to remember here:
then
constructs a new promise with the results of the executed handlerthen
chooses one of the supplied handlers based on the previous outcome... either fulfilled or rejected- A rejection will propogate until until it is handled
So in short, use fail
as a backstop at the end of a promise chain. We can then be sure that exceptions are handled... as long as fail
doesn't throw an exception :-(