Search code examples
javascriptangularjspromiseangular-http-interceptors

Detect existence of next handler in Angular JavaScript promise chain


Given the following two $resource examples:

var exampleOne = $resource('/path').save(objectOne);
exampleOne.$promise.then(function (success) {}, function (error) {});

var exampleTwo = $resource('/path').save(objectTwo);
exampleTwo.$promise.then(function (success) {});

[NOTE: Example two contains no error handler]

And an interceptor that sits below all $http requests:

var interceptor = ['$location', '$q', function ($location, $q) {
   function error(response) {
       if (response.status === 400) {
           return $q.reject(response);
       }
       else {
           $location.path('/error/page');
       }
       return $q.reject(response);
   }

   return {
       'responseError': error
   };
}

$httpProvider.interceptors.push(interceptor);

How can I make the interceptor not reject when the example resources $promise.then() contain no error callback? If the call back exists as in exampleOne then I wish to reject, but if not as in exampleTwo then I wish to redirect to the error page thus changing the conditional to something like:

if (response.status === 400 && $q.unresolvedPromises.doIndeedExist()) { ...

Why? Because only some situations in my project call for handling a 400 in a user friendly way, thus I'd like to eliminate many duplicate error callbacks or having to place a list of uncommon situations in the interceptor. I'd like the interceptor to be able to decide based on the presence of another handler in the promise chain.


Solution

  • Simply put it is impossible, you can't detect if someone will attach a handler in some point in the future just like you can't tell if when you throw in a function it will be caught on the outside or not. However, what you want done can be done.

    It is not a 'noob question', and it is very fundamental:

     function foo()
        throw new Error(); // I want to know if whoever is calling `foo`
                           // handles this error
     }
    

    First, what you can do

    Simply put in the first case:

     exampleOne.$promise.then(function (success) {}, function (error) {});
    

    What you get is a promise that is always fulfilled. However, in the second case the promise might be rejected. Handling a rejection with a rejection handler is like a catch in real code - once you handle it it is no longer rejected.

    Personally, I would not use an interceptor here, but rather a resource-using pattern since that's more clear with intent, you can wrap it in a function so it won't need a scope but I like that idea less. Here is what I'd do

    attempt(function(){
        return $resource('/path').save(objectTwo).$promise.
               then(function (success) {});
    });
    
    function attempt(fn){
        var res = fn();
        res.catch(function(err){
            // figure out what conditions you want here
            // if the promise is rejected. In your case check for http errors
            showModalScreen();
        }
        return res; // for chaining, catch handlers can still be added in the future, so
                    // this only detects `catch` on the function passed directly so 
                    // we keep composability
    }
    

    Now, a short proof that it can't be done

    Let's prove it for fun.

    Let's say we are given the code of a program M, we create a new promise p and replace every return statement in M andthrow statement in M with a return p.catch(function(){}) and also add a return p.catch(function(){}), now a handler will be added to p if and only if running M ever terminates. So in short - given code M we have constructed a way to see if it halts based on an existence of a solution to the problem of finding if catch is appended to p - so this problem is at least as hard as the halting problem.