Handling RESTful HTTP errors with promises

Preface

I understood the basic concept of promises before I started working at Expensify but I had never really used them in production code before. Since then, I've taken a lot of the things I learnt over to Uber with me and have applied that knowledge to the projects I'm working on there.

One of the things which has been most helpful is a way of handling errors by extending the promise spec which I learnt from Giorgio at Expensify. Today I'll share a simple implementation of a similar idea which I have used to good ends since.

For the sake of the article, we'll assume that we're using the Promise implementation included in jQuery. While this doesn't strictly adhere to the promises/A spec, I like to use it in examples because I feel it has the smallest barrier to entry for promise new-comers.

Error handling and promises

One of the great things about promises (or rather, deferreds) is that they encapsulate some knowledge of the result of the request being made. The promise object allows you to supply a done handler which will be run when a 200 is returned from the server, and a fail handler which is run when any error code is returned:

var doSomething = doSomethingAsync();  
doSomething.done(function(result){  
  // This function is run on HTTP 200
});
doSomething.fail(function(reason){  
  // This function is run on error (e.g. HTTP 403)
});
doSomething.always(function(){  
 // This function is always run
});

This is pretty nifty in and of itself. We are calling our async function and immediately being returned a promise object which we can attach handlers to to cover certain results of the request.

The classical approach to error handling

Now let's assume that our async function is failing and we want to know why. Let's take the trivial example of updating a user profile by submitting a form to a RESTful endpoint.

We'll say that we're user 1 and we want to update our name. We can post to /users/1 with a 'firstName' set to 'Will' in the body of the post and receive a 200 OK back from the server letting us know that the profile was update.

Our server only supports ASCII names, so posting with the value 'ウィル' for the key 'firstName' results in a 406 Not Acceptable from the server.

Updating another user's profile also isn't allowed, so if we tried to post to /users/2, the server responds with a 403 Forbidden.

Depending on the result from the server, we either want to highlight the firstName field of the form showing that the input was invalid, or show a message to the user saying that they can only update their own profile. That could look something like this:

var updateName = API.updateName('will');  
updateName.fail(function(reason){  
  var errorCode = reason.status;
  if(errorCode == 406){
    handleFormError(reason.responseText);
  }
  else if (errorCode == 403){
    handleUnauthorisedUser(reason.responseText);
  }
});

While this isn't too bad (and could clearly be written in a more concise way), as we add cases for other errors, this quickly becomes unwieldy when we're handling more than two error codes. What's more, if we want to have some generic handler for all 401 cases, we need to include that check in each error handler that we write.

We can do better. Enter HTTPDeferred.

What if we could extend the promise interface to enable us to handle specific errors? That way, we could define multiple error handlers at different levels and remove the need to constantly check errors codes.

Let's define a new method called handle which takes an array of status codes and a function. When any of the given status codes are found in the response from the server, that function should be run instead of the generic fail function. We could use such a method like this:

var updateName = API.updateName('will');  
updateName.handle([403], handleUnauthorisedUser);  
updateName.handle([406], handleFormError);  
updateName.fail(function(){  
  // Failed but wasn't 403 or 406.
});

Now we have a simple way to define functions which are run under specific cases and a good to define a catch-all handler for other errors.

We can achieve this functionality by wrapping the promise object in our own object which exposes an handle method:

  /**
   * A wrapper around our API calls, extending the ajax request handler with knowledge
   * about our error codes.
   * @param {Deferred} request The original request
   * @constructor
   */
  function HTTPDeferred(request){
    this.request = request;
    this.hasBeenHandled = false;
    return this;
  }

Here we define a new object which takes a request as a parameter and adds a simple flag to state whether the request has already been handled or not. We will use this object as a wrapper around the original request that we can control. Now we define our new handle function:

    /**
   * Allow the caller to handle any matching status codes with a given function.
   * @param {Number|Number[]} errorCodes The error codes to handle with the handle function
   * @param {Function} handleFunction The function to run for a matching error code.
   * @returns {Promise} The promise
   */
  HTTPDeferred.prototype.handle = function(errorCodes, handleFunction){
    errorCodes = _.isArray(errorCodes) ? errorCodes : [errorCodes];
    this.request.fail(_.bind(function(response){
      var errorCode = response.status;
      if(_.contains(errorCodes, errorCode)){
        var responseJSON = JSON.parse(response.responseText);
        handleFunction.call(this, responseJSON.message);
      }
    }, this));
    return this;
  };

This is the meat of our object. In this function, we add a fail handler to our original request and use it to check the resulting error codes. If one of our supplied handler codes match, we set a hasBeenHandled flag and call our handler function. We now just need a new implementation of fail to take that hasBeenHandled flag into account:

  /**
   * Proxy the fail function. If the fail has already been handled, do nothing.
   * @param {Function} failFunction Function to call when request fails
   * @returns {Promise} A promise
   */
  HTTPDeferred.prototype.fail = function(failFunction){
    this.request.fail(_.bind(function(response){
      if(this.hasBeenHandled){
        return;
      }
      var responseJSON = JSON.parse(response.responseText);
      failFunction.call(this, responseJSON.message);
    }, this));
    return this;
  };

Now, we can add fail functions to our promise which are only run when an error hasn't already been handled. The only thing that's left is to proxy the done and always functions to the internal request object:

  HTTPDeferred.prototype.done = function(doneFunction){
    this.request.done(doneFunction);
    return this;
  };

  HTTPDeferred.prototype.always = function(alwaysFunction){
    this.request.always(alwaysFunction);
    return this;
  };

Testing what we have so far.

In lieu of proper unit tests (for now), let's take a moment to make sure that what we've got so far is working as expected:

var someOperation = $.Deferred();  
var foo = new HTTPDeferred(someOperation)  
.done(function(){ console.log('Done'); })
.handle([401, 403], function(){
    console.log('Unauthorised');
})
.handle([406], function(){
    console.log('Invalid');
})
.fail(function(){ console.log('Fail'); });

And the results (running the above code before each line below):

someOperation.resolve(); // output: 'Done'

someOperation.rejectWith(this, [{status: 401, responseText: '{"message": ""}'}]); // output: 'Unauthorised'

someOperation.rejectWith(this, [{status: 406, responseText: '{"message": ""}'}]); // output: 'Invalid'

someOperation.rejectWith(this, [{status: 500}]); // output: 'Fail'  

Alright, looks like this is working well. The only thing that we haven't covered is the case where we add multiple handlers for the same code to the same request. In this case, we're going to run both handlers:

var someOperation = $.Deferred();  
var foo = new HTTPDeferred(someOperation)  
.handle([401], function(){
    console.log('First 401');
})
.handle([401], function(){
    console.log('Second 401');
})
.fail(function(){ console.log('Fail'); });

If we reject this operation with a 401 status code, we're going to see both 'First 401' and 'Second 401' printed. This is the expected behaviour (and aligns with the way promises usually work). It would also be trivial to stop running handlers based on the result of invoking the handleFunction, but I'm not 100% sure that adding that functionality is a good idea, so I'll leave it to the reader to extend the code if desired.

A real example

This is all very well and good, but where can we use this pattern in the real world? In the web app I'm currently writing at Uber, the server returns a 401 Unauthorised any time the user's authentication token is not valid or wasn't passed to the server. In this case, we always want to ask the user to reauthenticate themselves. To handle this case for every single request to the server, we wrap our ajax library (in the case, jQuery) like so:

// Keep a copy of the original $.ajax function
var _ajax = $.ajax;

registerGlobalErrorHandlers: function(){  
  $.ajax = _.wrap(_ajax, function(ajax, url, options){
    var request = new HTTPDeferred(ajax(url, options));

    // If we get a 401, redirect to the logic page.
    request.handle([401], function(){
      Backbone.Events.trigger(Events.auth.pleaseAuthenticate);
    });

    return request;
  });
}

Then, any time we contact the server, whether it's submitting a form or fetching a collection with Backbone, the 401 case is already handled.

Conclusion

There are a few downsides to this approach, the main one being that we do not offer an immutable promise through a promise method. Simply adding a promise method to proxy the original request would work, but then other parts of the code downstream wouldn't be able to use .handle. For now, that's a concession I'm willing to make.

Get the full source on GitHub