AngularJS: centralized application loading status handling using http interceptors

It’s a common need in a JavaScript application (especially a single page app) to show a spinner and/or a message while data is getting retrieved from the server (that is while the ajax call is in progress). In fact an ex colleague asked me how I manage this in AngularJS just a couple of weeks ago and I want to share my implementation since I think that’s the cleanest and more effective way to achieve the goal using native framework’s features. Moreover it’s also useful to handle ajax errors in a centralized and generic way and I’m gonna show that too.
AngularJS has a very useful feature: HTTP Interceptors, these ones are services that get automatically called on each ajax request step (before an ajax call, after an ajax call and so on) once they are registered using the $httpProvider.
So they are the perfect place where to write our logic for generic error handling and loading state, in fact the official documentation says:

For purposes of global error handling, authentication, or any kind of synchronous or asynchronous pre-processing of request or postprocessing of responses, it is desirable to be able to intercept requests before they are handed to the server and responses before they are handed over to the application code that initiated these requests. The interceptors leverage the promise APIs to fulfill this need for both synchronous and asynchronous pre-processing.

So what I do in my app is to create the following interceptor:

angular.module('myapp').
    service('LoadingInterceptor', 
    ['$q', '$rootScope', '$log', 
    function($q, $rootScope, $log) {
        'use strict';

        return {
            request: function(config) {
                $rootScope.loading = true;
                return config;
            },
            requestError: function(rejection) {
                $rootScope.loading = false;
                $log.error('Request error:', rejection);
                return $q.reject(rejection);
            },
            response: function(response) {
                $rootScope.loading = false;
                return response;
            },
            responseError: function(rejection) {
                $rootScope.loading = false;
                $log.error('Response error:', rejection);
                return $q.reject(rejection);
            }
        };
    }]);

It makes use of $rootScope to store the loading state “globally”, which is set to true as soon the request is created (request method) and set to false when the request is invalid (requestError) or the call has been completed (response or responseError).
It also logs errors in case of requestError and responseError.

Then I register the interceptor in the module config() block:

.config(['$httpProvider', function($httpProvider) {
    $httpProvider.interceptors.push('LoadingInterceptor');
}]);

As you can see it’s registered as a simple plain string rather than using the usual dependency injection (AngularJS knows how to properly load the service).

Regarding the display of a message/spinner to the user, in my templates all I have to do is something like:

<div data-ng-if="loading">
    Loading...
</div>

That’s really cool! clean! simple! effective! :)

UPDATE:

I realized that my interceptor doesn’t work properly when concurrent xhr requests are involved.
For example, if a request A starts and then a request B starts, if B completes its job before A, the loading status is defined as “false“. To fix this issue I modified it by tracking the count of initialized requests and completed ones, so the final reliable implementation of the interceptor is the following:

angular.module('crs').service('LoadingInterceptor', ['$q', '$rootScope', '$log', 
function ($q, $rootScope, $log) {
    'use strict';

    var xhrCreations = 0;
    var xhrResolutions = 0;

    function isLoading() {
        return xhrResolutions < xhrCreations;
    }

    function updateStatus() {
        $rootScope.loading = isLoading();
    }

    return {
        request: function (config) {
            xhrCreations++;
            updateStatus();
            return config;
        },
        requestError: function (rejection) {
            xhrResolutions++;
            updateStatus();
            $log.error('Request error:', rejection);
            return $q.reject(rejection);
        },
        response: function (response) {
            xhrResolutions++;
            updateStatus();
            return response;
        },
        responseError: function (rejection) {
            xhrResolutions++;
            updateStatus();
            $log.error('Response error:', rejection);
            return $q.reject(rejection);
        }
    };
}]);