Lately during development at one of our clients, Ravello Systems, we decided we wanted better HTTP error handling.

Basically, our perfect solution would have generic handlers for errors, and most calls in the code will not have to do any special work for handling errors. This means that things like authentication problems, server unavailability issues, etc. will be handled in one place — like adding a generic “something went wrong” modal.

But, we also wanted to be able to easily override these default handlers so that specific places can do things like silently ignore errors of unimportant calls (e.g. background requests that aren’t user-facing), or display a special “try again later” context—aware message.

This is a case study of how we currently implemented this. I’m sharing this for others that might need it and in hopes to hear of alternative ways to accomplish it.

Our requirements

The generic error handling should happen automatically, and can’t be something a developer can forget to enable. That means things like adding a ‘.catch(genericHandler)’ to every call is out of the question.

When the less common case happens, meaning someone isn’t using the generic handler, it should be explicit.

It should work seamlessly with angular’s $http service so that no matter if we aren’t the ones making the calls ourselves, everything should still be handled.

It should allow us to easily handle grouped requests (i.e. requests that are handled by the caller in a single $q.all() call).

How the end result looks

If you’re making a call that just wants to use the generic handling code, it looks like this:

1 2 3 4 5 $http . get ( 'some/url' ). then ( function ( response ) { // Stuff here } );

Seems familiar? That’s right, in most cases you don’t need to change your code at all.

And in cases you do want to handle errors yourself:

1 2 3 4 5 6 7 8 RequestsErrorHandler . specificallyHandled ( function () { $q . all ({ foo : FooService . fetch (), bar : BarService . fetch ()}). then ( function () { /* Handle success */ }, function () { /* Handle specific errors */ } ); } );

As you can see, it’s pretty straightforward. And the use of a function (which we would essentially consider a block in languages like Ruby) allows us to group multiple requests in the same error handler.

The implementation

Basically we tag every request that needs to be generically handled by adding a HTTP header to it. Then when requests fail due to errors an interceptor handles those that are tagged.

There are 3 parts:

A decorator for $http – This is the only way we came up with to tag requests inside our specificallyHandled function. Interceptors are run asynchronously and so can’t tell whether the request was made inside our specificallyHandled block or not. This decorator simply wraps all the $http functions to add a specific header in cases they should be generically handled.

Response interceptor – This intercepts all the failed responses and handles them in a generic way – but only if they have the magical HTTP header we add in the $http decorator.

RequestsErrorHandler – A service that turns on or off the generic handling according to when it is called.

The Code

Also available here.

var HEADER_NAME = 'MyApp-Handle-Errors-Generically'; var specificallyHandleInProgress = false; angular.module('myApp').factory('RequestsErrorHandler', ['$q', function($q) { return { // --- The user's API for claiming responsiblity for requests --- specificallyHandled: function(specificallyHandledBlock) { specificallyHandleInProgress = true; try { return specificallyHandledBlock(); } finally { specificallyHandleInProgress = false; } }, // --- Response interceptor for handling errors generically --- responseError: function(rejection) { var shouldHandle = (rejection && rejection.config && rejection.config.headers && rejection.config.headers[HEADER_NAME]); if (shouldHandle) { // --- Your generic error handling goes here --- } return $q.reject(rejection); } }; }]); angular.module('myApp').config(['$provide', '$httpProvider', function($provide, $httpProvider) { $httpProvider.interceptors.push('RequestsErrorHandler'); // --- Decorate $http to add a special header by default --- function addHeaderToConfig(config) { config = config || {}; config.headers = config.headers || {}; // Add the header unless user asked to handle errors himself if (!specificallyHandleInProgress) { config.headers[HEADER_NAME] = true; } return config; } // The rest here is mostly boilerplate needed to decorate $http safely $provide.decorator('$http', ['$delegate', function($delegate) { function decorateRegularCall(method) { return function(url, config) { return $delegate[method](url, addHeaderToConfig(config)); }; } function decorateDataCall(method) { return function(url, data, config) { return $delegate[method](url, data, addHeaderToConfig(config)); }; } function copyNotOverriddenAttributes(newHttp) { for (var attr in $delegate) { if (!newHttp.hasOwnProperty(attr)) { if (typeof($delegate[attr]) === 'function') { newHttp[attr] = function() { return $delegate.apply($delegate, arguments); }; } else { newHttp[attr] = $delegate[attr]; } } } } var newHttp = function(config) { return $delegate(addHeaderToConfig(config)); }; newHttp.get = decorateRegularCall('get'); newHttp.delete = decorateRegularCall('delete'); newHttp.head = decorateRegularCall('head'); newHttp.jsonp = decorateRegularCall('jsonp'); newHttp.post = decorateDataCall('post'); newHttp.put = decorateDataCall('put'); copyNotOverriddenAttributes(newHttp); return newHttp; }]); }]);