Global Error handler that only catches "unhandled" promises
Asked Answered
C

1

10

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;
                }
            });
Clio answered 29/11, 2015 at 2:55 Comment(2)
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.Demimonde
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.Clio
T
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.
             })
    }
}
Tuppeny answered 3/1, 2016 at 21:58 Comment(6)
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.Clio
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.Tuppeny
right, but I'm saying I don't want to inject $q into every place I'm using $httpClio
you don't have other choise.Tuppeny
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.Tuppeny
I did a 3rd solution, check it out pleaseTuppeny

© 2022 - 2024 — McMap. All rights reserved.