AngularJS: Tips about Promises and $http Service
Hey all, everything all right?
First of all, I’d like to thank all support and those that have questions, for sure I’d like to be able to respond to everyone, or have all the answers, however, sometimes I neither have time enough or just don’t know the answer.
Currently, I’ve been reviewing a lot of Angular code lately, and received some curious emails about a problem I found many times during my reviews that it’s the unnecessary use of the deferred object to resolve or reject promises. Yes, that’s exactly what I’ll talk about today, and yes, I know it may not be the most useful thing possible, but since I had a question about it and also I always see some unnecessary code, I want to teach a simplified approach.
Another problem is always mixed then()
and catch()
with two callbacks in the then()
, and sometimes even success()
and error()
in the return of the $http service . I will try to be as quickly as possible with the examples, proposing a single style that can be applied to any other promise library in the future.
Resolving Promises
The code below use the $http service to make a REST call and return only the data property of the response. This is a common implementation that I usually find in AngularJS code, and I know that works, but let’s see how to improve it.
function getData($q, $http) {
var deferred = $q.defer();
$http.get('...')
.then(function(response) {
deferred.resolve(response.data);
});
return $q.promise;
}
The code above could be simplified like this:
// Better
function getData($http) {
return $http.get('...')
.then(function(response) {
return response.data;
});
}
The then()
function accepts three callback parameters: successCallback, errorCallback, and notifyCallback
So, If you return a value on successCallback or errorCallback, the return will be used to resolve the promise. Yes, you don’t need to explicitly deferred.resolve(response.data)
every time to solve promises, instead, you can simply return response.data in then()
’s callback.
The then()
function returns a new promise, so isn’t need to use $q
to create a deferred object and return a promise. Just return the promise created by then()
.
Rejecting Promises
What about error handling? Let’s use the example above, we can call deferred.reject
as errorCallback.
function getData($q, $http) {
var deferred = $q.defer();
$http.get('...')
.then(function(response) {
deferred.resolve(response.data);
},
function(err){
deferred.reject(err);
});
return $q.promise;
}
Now we can think in a simplified solution and just return something instead of reject right? But by doing so, what we return? You probably think that we could return the error as in successCallback:
function getData($http) {
$http.get('...')
.then(function(response) {
return response.data;
},
function(err){
return err; // Wrong
});
}
As I said before, the returned values from the successCallback and errorCallback are considered values resolved for the promise. So the returned response in the errorCallback will solve the promise instead reject it. Even if you return a false
value or nothing, the value will be considered resolved. You can use another way if necessary with $q.reject(err)
:
// Better
function getData($q, $http) {
$http.get('...')
.then(function(response) {
return response.data;
},
function(err){
return $q.reject(err);
});
}
Besides the errorCallback as second parameter, the promise has another method called catch()
to handle errors. I personally prefer to use then()
and catch()
together over the two callback functions in then()
because of readability.
// Even Better
function getData($q, $http) {
$http.get('...')
.then(function(response) {
return response.data;
})
.catch(function(err){
return $q.reject(err);
});
}
This way, then()
is used only for successCallback, and catch()
is basically used for errors applied as if it were then()
without the successCallback – then(angular.noop, errorCallback)
The biggest advantage is that we can avoid callback hell by chaining promises using the then()
and handle any errors that could happen during the process in a single catch()
:
function getData($q, $http) {
$http.get('...')
.then(function(response) {
return DataService.validateData(response.data);
})
.then(function(data) {
return DataService.updateOffilineStorage(data);
})
.catch(function(err){
return $q.reject(err);
});
}
Chaining promises from services like the DataService
above that returns us promises, make our code more elegant and simple to read and capturing any errors in a single point.
Success and Errors in $http Service
In old versions of AngularJS (1.3.x and earlier) $http service has two additional methods success()
and error()
that are similar to then()
, but they have some differences :
$http.get('...')
.success(function(data) {
...
})
.error(function(err) {
...
})
If you have remembered jQuery and how simple things were at that time, forget about it, since version 1.4.x these two methods have been obsolete because of the confusion they introduced.
The callback you pass both success()
and error()
is not a response object, but the data itself (as in jQuery), and inside the callback is no longer necessary to return response.data
to get only the data. An example:
// Success
$http.get('...')
.success(function(data) {
$log(data)
});
// Then
$http.get('...')
.then(function(response) {
$log(response.data)
});
Yes, it’s a silly difference, but neithersuccess()
or error()
will return a new promise, so I strongly recommend adopt then()
and catch()
since they can be found in other Promise libraries as a convention.
Conclusion
The use of a deferred object can be replaced by simple things:
- Return a value creating a new promise when it resolves.
- Return a rejected promise using
$q.reject()
to reject the promise.
Prefira utilizar catch()
para fornecer erroCallbacks e facilitar o encadeamento promises ao invés de dois callbacks dentro de then()
. e pare de utilizar success()
e error()
.
Finally, prefer using catch()
to provide erroCallbacks and use the power of chained promises instead than two callbacks in then()
. And stop using success()
and error()
, please!
Join the Conversation