David Cai | Blog

Tips on Promise and $http Service

Mon Sep 14, 2015

Promise

I’ve been reviewing a lot of Angular code lately. One common problem I found is the unnecessary use of deferred object to resolve and reject promises. In this post, I will illustrate a simplified approach. Another problem is a mix of then, catch, two callbacks in then, success, and error. I will go through each, and propose adopting a single style that can also be applied when using other Promise libraries.


Resolve Promise

The following snippet uses $http service to make a REST call, and resolve the returned data from the response. This is a common pattern I often find in Angular code, however, the code is not optimal (see below).

/////////////////////////////////////////////////
// Not preferred. Unnecessary usage of deferred
/////////////////////////////////////////////////
function getUser($q, $http) {
  var deferred = $q.defer();

  $http.get('...').then(function(response) {
    deferred.resolve(response.data);
  });

  return $q.promise;
}

The above code can be simplified to this:

//////////////
// Preferred
//////////////
function getUser($http) {
  return $http.get('...').then(function(response) {
    return response.data;
  });
}

First, the then function accepts three callbacks: successCallback, errorCallback, and notifyCallback. If you return a value in the successCallback or errorCallback, the returned value will be used to resolve the promise. You don’t have to explicitly call deferred.resolve(response.data) to resolve the promise. Instead, simply return response.data in then’s callback.

Second, the then function returns a new promise, so no need to use $q to create a deferred object and return its associated promise. Just return the promise created by then.


Reject Promise

What about error handling? Using the not preferred example, we can call deferred.reject in the error callback to reject the promise:

/////////////////////////////////////////////////
// Not preferred. Unnecessary usage of deferred
/////////////////////////////////////////////////
function getUser($q, $http) {
  var deferred = $q.defer();

  $http.get('...').then(
    function(response) { // Success callback
      deferred.resolve(response.data); // Resolve
    },
    function(response) { // Error callback
      deferred.reject(response); // Reject
    }
  );

  return $q.promise;
}

However, a simplified solution is again to use return instead of reject. But, what shall we return? You probably think to return the response in the error callback like this:

function getUser($http) {
  return $http.get('...').then(
    function(response) {
      return response.data;
    },
    function(response) { // Error callback
      return response; // <<< Wrong
      // Or return false; // <<< Still wrong
    }
  );
}

As I mentioned above, values returned from the successCallback or the errorCallback are considered to be resolved values for the promise. So the return response in the error callback will resolve instead reject the promise. Even when the returned value is falsy, the falsy value will be considered as a resolved value. The correct way is to return a rejected promise using $q.reject(response):

//////////////
// Preferred
//////////////
function getUser($q, $http) {
  return $http.get('...').then(
    function(response) {
      return response.data;
    },
    function(response) { // Error callback
      return $q.reject(response); // Use $q.reject
    }
  );
}

Besides the errorCallback in then function, Promise has another method – catch to take an error callback. I personally prefer the then and catch pair over the two callbacks in then for its readability.

///////////////////////
// Catch is preferred
///////////////////////
function getUser($q, $http) {
  return $http.get('...').then(
    function(response) {
      return response.data;
    }
  ).catch( // Catch
    function(response) {
      return $q.reject(response);
    }
  );
}

In the above code, then is used only for success callback. A catch is basically a then() without success callback – then(angular.noop, errorCallback).

The difference from then(successCallback, errorCallback) is that catch is chained of the promise returned by then. However, the errorCallback in then is chained of the original promise returned by $http.get().

Please be advised that in IE8 catch has special meaning, so instead of the dot syntax, write ['catch']. Yes, in IE8, catch is the catch :)

/////////////////
// Catch in IE8
/////////////////
function getUser($q, $http) {
  return $http.get('...').then(
    function(response) {
      return response.data;
    }
  )['catch']( // Catch
    function(response) {
      return $q.reject(response);
    }
  );
}

If you don’t need to handle the error in the getUser function shown above, you can drop the entire error callback, and let getUser’s consuming code to handle the error:

//////////////
// Preferred
//////////////
function getUser($q, $http) {
  return $http.get('...').then(function(response) {
    return response.data;
  });
}

// getUser's consuming code
getUser().then(
  function(data) { // Success callback
    // Do something about data
  },
  function(response) { // Error callback
    // Handle error state
  }
);


Success and Error in $http Service

In old versions (1.3 and less) of Angular, $http service has two additional methods – success() and error(), which are similar to then(), however, the subtle differences are important:

$http.get('')
  .success(function(data) {
    ...
  })
  .error(function(error) {
    ...
  });

These two methods are deprecated in Angular 1.4 partially for the confusion they introduced. The first argument passed into success and error callbacks is not the response object itself, instead, it is the data unwrapped from the response object. However, in then callback, you call response.data to retrieve the data. Here is a comparative example:

// Success method
$http.get('').success(function(data) {
  $log.log(data);
});

// Then method
$http.get('').then(function(response) {
  $log.log(response.data); // <<< See the difference?
});

The second difference is that unlike then, neither success nor error will create and return new promises. The promise they return is the same HTTP promise from $http.get().

Overall, I’d recommend not using success and error, but adopting then and catch. The latter can be found in other Promise libraries, and is already evolved to be a convention.


Conclusion

The usage of a deferred object to resolve and reject promises can be replaced by a simplified style:

  • Returning a value in success or error callbacks resolves the promise with the returned value.

  • Returning a rejected promise using return $q.reject(something) rejects the promise.

Prefer using catch to provide error callbacks. Change .catch to ['catch'] in IE8.

Stop using success and error.



« »

comments powered by Disqus