9

I have a global error handler for my angular app which is written as an $http interceptor, but I'd like to take it a step further. What I'd like is for each $http call that fails (is rejected), any "chained" consumers of the promise should first try to resolve the error, and if it is STILL unresolved (not caught), THEN I'd like the global error handler to take over.

Use case is, my global error handler shows a growl "alert box" at the top of the screen. But I have a couple of modals that pop up, and I handle the errors explicitly there, showing an error message in the modal itself. So, essentially, this modal controller should mark the rejected promise as "handled". But since the interceptor always seems to be the first to run on an $http error, I can't figure out a way to do it.

Here is my interceptor code:

angular.module("globalErrors", ['angular-growl', 'ngAnimate'])
    .factory("myHttpInterceptor", ['$q', '$log', '$location', '$rootScope', 'growl', 'growlMessages',
        function ($q, $log, $location, $rootScope, growl, growlMessages) {
            var numLoading = 0;
            return {
                request: function (config) {
                    if (config.showLoader !== false) {
                        numLoading++;
                        $rootScope.loading = true;
                    }
                    return config || $q.when(config)
                },
                response: function (response) {
                    if (response.config.showLoader !== false) {
                        numLoading--;
                        $rootScope.loading = numLoading > 0;
                    }
                    if(growlMessages.getAllMessages().length) { // clear messages on next success XHR
                        growlMessages.destroyAllMessages();
                    }
                    return response || $q.when(response);
                },
                responseError: function (rejection) {
                    //$log.debug("error with status " + rejection.status + " and data: " + rejection.data['message']);
                    numLoading--;
                    $rootScope.loading = numLoading > 0;
                    switch (rejection.status) {
                        case 401:
                            document.location = "/auth/login";
                            growl.error("You are not logged in!");
                            break;
                        case 403:
                            growl.error("You don't have the right to do this: " + rejection.data);
                            break;
                        case 0:
                            growl.error("No connection, internet is down?");
                            break;
                        default:
                            if(!rejection.handled) {
                                if (rejection.data && rejection.data['message']) {
                                    var mes = rejection.data['message'];
                                    if (rejection.data.errors) {
                                        for (var k in rejection.data.errors) {
                                            mes += "<br/>" + rejection.data.errors[k];
                                        }
                                    }
                                    growl.error("" + mes);
                                } else {
                                    growl.error("There was an unknown error processing your request");
                                }
                            }
                            break;
                    }
                    return $q.reject(rejection);
                }
            };
        }]).config(function ($provide, $httpProvider) {
        return $httpProvider.interceptors.push('myHttpInterceptor');
    })

This is rough code of how I'd expect the modal promise call to look like:

$http.get('/some/url').then(function(c) {
                $uibModalInstance.close(c);
            }, function(resp) {
                if(resp.data.errors) {
                    $scope.errors = resp.data.errors;
                    resp.handled = true;
                    return resp;
                }
            });
AVI
  • 5,516
  • 5
  • 29
  • 38
Steven M
  • 574
  • 3
  • 18
  • Have you thought of implementing it on server side instead? Also when you say it should *try to resolve*, can you give an example of it. – Rajesh Dec 02 '15 at 05:43
  • Can't do it on the server, the whole point is to be working with the promises client side. By try to resolve, I mean that the global error handler should be the LAST catchall for errors in an http promise. Currently it is the first thing to run upon an error. – Steven M Dec 02 '15 at 18:22

1 Answers1

2

1. Solution (hacky way)

You can do that by creating a service doing that for you. Because promises are chain-able and you basically mark a property handled at the controller level, you should pass this promise to your service and it'll take care of the unhandled errors.

myService.check(
    $http.get('url/to/the/endpoint')
             .then( succCallback, errorCallback) 
);

2. Solution (preferred way)

Or the better solution would be to create a wrapper for $http and do something like this:

myhttp.get('url/to/the/endpoint', successCallback, failedCallback);

function successCallback(){ ... }
function failedCallback(resp){
    //optional solution, you can even say resp.handled = true
    myhttp.setAsHandled(resp);

    //do not forget to reject here, otherwise the chained promise will be recognised as a resolved promise.
    $q.reject(resp);
}

Here the myhttp service call will apply the given success and failed callbacks and then it can chain his own faild callback and check if the handled property is true or false.

The myhttp service implementation (updated, added setAsHandled function which is just optional but it's a nicer solution since it keeps everything in one place (the attribute 'handled' easily changeable and in one place):

function myhttp($http){
    var service = this;

    service.setAsHandled = setAsHandled;
    service.get = get;

    function setAsHandled(resp){
        resp.handled = true;
    }

    function get(url, successHandler, failedHandler){
        $http.get(url)
             .then(successHandler, failedHandler)
             .then(null, function(resp){
                  if(resp.handled !== true){
                       //your awesome popup message triggers here.
                  }
             })
    }
}

3. Solution

Same as #2 but less code needed to achieve the same:

myhttp.get('url/to/the/endpoint', successCallback, failedCallback);

function successCallback(){ ... }
function failedCallback(resp){
    //if you provide a failedCallback, and you still want to have  your popup, then you need  your reject.
    $q.reject(resp);
}

Other example:

//since you didn't provide failed callback, it'll treat as a non-handled promise, and you'll have your popup.
myhttp.get('url/to/the/endpoint', successCallback);

function successCallback(){ ... }

The myhttp service implementation:

function myhttp($http){
    var service = this;

    service.get = get;

    function get(url, successHandler, failedHandler){
        $http.get(url)
             .then(successHandler, failedHandler)
             .then(null, function(){ 
                 //your awesome popup message triggers here.
             })
    }
}
Iamisti
  • 1,680
  • 15
  • 30
  • This could work... I'd want to remove the dependency on ```$q``` everywhere I'm using ```$http``` though... probably easy enough to just do the reject in the ```setAsHandled``` service method. – Steven M Jan 06 '16 at 18:13
  • no, you will need your $q, since you have to reject your promise in your failed handler. Otherwise in your `get` service function it will not catch the failure case of it. – Iamisti Jan 06 '16 at 18:29
  • right, but I'm saying I don't want to inject ```$q``` into every place I'm using ```$http``` – Steven M Jan 07 '16 at 05:25
  • you don't have other choise. – Iamisti Jan 07 '16 at 10:20
  • well if it's handled you don't need to reject your promise, and it won't do the popup. Actually you can skip `myhttp.setHandled` function call as well. So the basic rule will be: if you reject a promise in your failed handler, it will do the popup, otherwise it won't. So even if you don't have a failed callback it'll do the popup since it's not handled. Hope it all makes sense. – Iamisti Jan 07 '16 at 10:22
  • I did a 3rd solution, check it out please – Iamisti Jan 07 '16 at 10:27