Properly cascading rejected Angular.js promises

development

Properly cascading rejected Angular.js promises

Angular.js promises offer a nice design pattern for asynchronous events. The way many developers often use them in angular apps looks like this

var result = $http.get('/url').then(
  function success(data) {
    console.log(data);
    return data;
  },
  function failure(reason) {
    console.log(reason);
    return null;
  }
);

In this use-case I am returning the data passed to the callback function in the case of success and returning null in the case of a failure. In both cases the result is logged to the console. The result variable will probably hold some JSON or be null.

The key point to notice here is that $http.get(); is returning a promise which we call then(success, failure); on.

My use-case is a slightly different and looks like this

function apiCall() {
  return $http.get('/url').then(
    function success(data) {
      console.log(data);
      return data;
    },
    function failure(reason) {
      console.log(reason);
      return reason;
    }
  );
}

Here you can see the function apiCall(); wraps and returns the result of the $http.get(); call. This means that apiCall(); is itself returning a promise, and the code in the success(data); function could return the result of another $http.get(); that is dependent on the data returned by the first one.

When I was testing ajax calls that I knew would fail I wasn’t getting the result I was expecting from apiCall(); as in the example below.

apiCall().then(
  function success(data) {
    console.log('Returned success from apiCall ' + data.stringify());
  },
  function failure(reason) {
    console.log('Returned failure from apiCall ' + reason);
  }
);

What was occurring was that I was never seeing the console.log(); execute in the above failure(reason); call. The success console.log(data); was always running instead. I discovered that the failure(); function passed to then(); does not return a rejected promise because it is assumed all errors are handled in the failure(); function call, so the value reason returned within the failure(); function call is the raw error, not a promise.

To continue returning a rejected promise for any later then(); calls you must explicitly return $q.reject(reason); in your failure(); function call.

$q.reject(reason) Creates a promise that is resolved as rejected with the specified reason. This API should be used to forward rejection in a chain of promises. If you are dealing with the last promise in a promise chain, you don’t need to worry about it.

This also means that if you “catch” an error via a promise error callback and you want to forward the error to the promise derived from the current promise, you have to “rethrow” the error by returning a rejection constructed via reject.

This guarantees that you are returning a rejected promise and you can continue chaining then(); calls and expect that your failure will cascade so that you can perform different operations on it (or in my case, display different errors to the user) when an ajax call fails as seen below.

function apiCall() {
  return $http.get('/url').then(
    function success(data) {
      console.log('Returned success from $http.get() ' + data.stringify());
      return data;
    },
    function failure(reason) {
      console.log('Returned failure from $http.get() ' + reason);
      return $q.reject(reason);
    }
  );
}

apiCall()
  .then(
    function success(data) {
      console.log('Returned success from apiCall ' + data.stringify());

      return $http.get('/otherUrl');
    },
    function failure(reason) {
      console.log('Returned failure from apiCall ' + reason);
    }
  )
  .then(
    function success(data) {
      console.log(
        'Returned success from second $http.get() ' + data.stringify()
      );
    },
    function failure(reason) {
      console.log('Returned failure from second $http.get() ' + reason);
    }
  );