Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Global Error handler that only catches "unhandled" promises

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;
                }
            });
like image 381
Steven M Avatar asked Nov 29 '15 02:11

Steven M


People also ask

How do you handle unhandled Promise rejection?

If an error condition arises inside a promise, you “reject” the promise by calling the reject() function with an error. To handle a promise rejection, you pass a callback to the catch() function. This is a simple example, so catching the rejection is trivial.

How do you catch an error in a Promise?

catch " around the executor automatically catches the error and turns it into rejected promise. This happens not only in the executor function, but in its handlers as well. If we throw inside a . then handler, that means a rejected promise, so the control jumps to the nearest error handler.

What is unhandled Promise rejection error?

This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with . catch(). (rejection id: 1) (node:31851) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated.

Can we use try catch inside Promise?

If you throw an error inside the promise, the catch() method will catch it, not the try/catch. In this example, if any error in the promise1, promise2, or promise4, the catch() method will handle it.


1 Answers

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.
             })
    }
}
like image 75
Iamisti Avatar answered Oct 14 '22 01:10

Iamisti