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:

  1. then constructs a new promise with the results of the executed handler
  2. then chooses one of the supplied handlers based on the previous outcome... either fulfilled or rejected
  3. 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 :-(

comments powered by Disqus