Friday, December 30, 2016

More traps, anti-patterns and tips about AngularJS promises - Ninja Squad

More traps, anti-patterns and tips about AngularJS promises - Ninja Squad

In my previous post, I showed some common traps and anti-patterns that many newbies, including myself, fall into when using basic functionalities of promises.

Let’s continue this journey and see how we can use them in more complex situations.

Chain chain chain
Imagine the following scenario: you’re displaying a form allowing to edit a question of a quiz. The user should be able to click Next to save the current question and have the page display the next question in the edit form.

The Next button thus has several responsibilities:

check if the form is dirty. If not, continue without saving (i.e. skip step 2).
save the form. If the save fails due to a server-side error, stop and stay on the current question (i.e. skip step 3).
show the next question.
That is a mix of synchronous operations and asynchronous ones. Promises are for asynchronous operations. So let’s write the code:

$scope.next = function() {
if (formIsDirty()) {
saveQuestion();
}
getNextQuestion().then(function(question) {
$scope.question = question;
});
};
Oops. Once again, that would be right if saveQuestion() was a synchronous, blocking operation, throwing an exception if the save failed. But that’s not the case. The above code gets the next question before knowing if the save has been successful.

So change the code:

$scope.next = function() {
if (formIsDirty()) {
saveQuestion().then(function() {
getNextQuestion().then(function(question) {
$scope.question = question;
});
});
}
else {
getNextQuestion().then(function(question) {
$scope.question = question;
});
}
};
That’s ugly. We’re repeating the same block of code twice. We now know that trying to transform an asynchronous call into a synchronous, blocking call is a dead-end. But we could do the reverse thing: transform a synchronous call into an asynchronous one:

$scope.next = function() {
saveQuestionIfDirty().then(function() {
getNextQuestion().then(function(question) {
$scope.question = question;
});
});
};

var saveQuestionIfDirty = function() {
if (formIsDirty()) {
return saveQuestion();
}
else {
var defer = $q.defer();
defer.resolve('no need to save the question');
return defer.promise;
}
};
Well, this code is still not great. First of all, 3 lines of code just to create a resolved promise. Surely Angular allows doing that in an easier way. Let’s look at the documentation.

when(value);
[…]
Returns a promise of the passed value or promise
So we can simplify saveQuestionIfDirty():

var saveQuestionIfDirty = function() {
if (formIsDirty()) {
return saveQuestion();
}
else {
return $q.when('no need to save the question');
}
};
Let’s look at next() now. Weren’t promises supposed to avoid this pyramid of callbacks?

Here’s how I would like to write the code:

$scope.next = function() {
saveQuestionIfDirty().then(function() {
getNextQuestion();
}).then(function(question) {
$scope.question = question;
});
};
But that won’t work. First of all, we’ve seen before that the promise returned by then()is resolved via the value returned by the callback. And our callback doesn’t return anything. It should thus return the next question.

$scope.next = function() {
saveQuestionIfDirty().then(function() {
return getNextQuestion();
}).then(function(question) {
$scope.question = question;
});
};
Wait a minute. Due to asynchronism, once again, there’s no way for getNextQuestion() to return a question. All it can return is a promise of question. So the above code won’t work, right?

Not right. Let’s have a look at the documentation again:

then(successCallback, errorCallback, notifyCallback)
[…]
This method returns a new promise which is resolved or rejected via the return value of the successCallback, errorCallback (unless that value is a promise, in which case it is resolved with the value which is resolved in that promise using promise chaining).
(emphasis mine)

Isn’t that crystal clear? No, quite frankly, it’s not. Let’s try to explain this with our example.

If you return a question from the then() callback, as we learnt in the previous post, then() will return a promise of this question.

But if you return a promise of question from the then callback, then() won’t return a promise of promise of question as you could imagine. It will “flatten” the result and also return a promise of question. Which thus makes our last implementation of $scope.next() correct.

Rejecting
Now let’s say we would like to display an error message when saving the question fails. You remember that we can use catch() to register an error callback. catch(fn) is just an alias for then(null, fn).

var saveQuestion = function() {
return $http.post(...).catch(function(response) {
$scope.saveErrorDisplayed = true;
});
};
That’s wrong again. The callback doesn’t return anything. Which actually means it returns undefined. So you might think that it’s not too bad: saveQuestion() will return a rejected promise, and the rejection value will be undefined. Since the rest of the code doesn’t care about the rejection value, that’s fine. Well, nope. Returning a value from the callback resolves the promise returned by saveQuestion() even if you return this value from an error callback. The original rejected promise of HTTP response is thus “transformed” into a resolved promise of undefined.

That’s something that can be useful (we’ll see an example soon), but which is undesired in that case. So how can we transform the rejected promise into another rejected promise? By chaining, again. Instead of returning a value, we can simply return a rejected promise. Just as $q.when() allows creating a resolved promise, $q.reject() allows creating a rejected promise:

var saveQuestion = function() {
return $http.post(...).catch(function(response) {
$scope.saveErrorDisplayed = true;
return $q.reject(response);
});
};
There is another way to do that, but it has a nasty side-effect on unit tests, which is why I wouldn’d recommend it: throwing the rejection:

var saveQuestion = function() {
return $http.post(...).catch(function(response) {
$scope.saveErrorDisplayed = true;
throw response;
});
};
Recap on chaining
original promise is resolved
success callback returns value or resolved promise of value

⇒ then() returns a resolved promise of value

success callback returns rejected promise of value or throws a value

⇒ then() returns a rejected promise of value

success callback is absent

⇒ then() returns a promise resolved as the original

original promise is rejected
error callback returns value or resolved promise of value

⇒ then() returns a resolved promise of value

error callback returns rejected promise of value or throws a value

⇒ then() returns a rejected promise of value

error callback is absent

⇒ then() returns a promise rejected as the original

Here’s a plunkr showing a suite of unit tests demonstrating all these cases.

Testing is doubting
OK. Now let’s say we have a service returning a promise of ponies, and we want to test a controller $scope function that stores the ponies in the scope, or an error flag if the promise is rejected. Simplest thing you can imagine.

it('should set ponies in $scope if ponies can be loaded', function() {
var ponies = ['Aloe', 'Pinkie Pie'];
spyOn(ponyService, 'getPonies')
.andReturn($q.when(ponies));

$scope.showPonies();

expect($scope.ponies).toBe(ponies);
});

it('should store an error flag in the scope', function() {
spyOn(ponyService, 'getPonies')
.andReturn($q.reject('error'));

$scope.showPonies();

expect($scope.errorLoadingPonies).toBeTruthy();
});
These tests should pass, right?

Nope. Callbacks are not invoked as soon as the promise is resolved or rejected. Even if the promise is already resolved or rejected and a new callback is passed to then(), this callback won’t be invoked immediately. AngularJS only invokes the then() callbacks at the next digest loop. This doesn’t make much difference in classical application code, but it does make a huge one in unit tests. You need to explicitely call $digest() or $apply()on a $scope to force AngularJS to invoke the callbacks:

it('should set ponies in $scope if ponies can be loaded', function() {
var ponies = ['Aloe', 'Pinkie Pie'];
spyOn(ponyService, 'getPonies')
.andReturn($q.when(ponies));

$scope.showPonies();

$scope.$apply();

expect($scope.ponies).toBe(ponies);
});

it('should store an error flag in the scope', function() {
spyOn(ponyService, 'getPonies')
.andReturn($q.reject('error'));

$scope.showPonies();

$scope.$apply();

expect($scope.errorLoadingPonies).toBeTruthy();
});
If you’re testing a service (which doesn’t use a $scope), call $apply() on the $rootScope service.

Conclusion
Promises are a powerful concept, but a quite hard one to grasp. And I’ve not even talked about composition, which allows executing several asynchronous calls in parallel, and getting the result once all the promises are resolved.

But mastering them tremendously helps in writing elegant, robust code in AngularJS applications. Promises are also coming in ECMAScript 6, and even if the syntax used to create them is different, their behavior is identical. So even VanillaJS code will soon use promises.

I would have liked to have such an article when I started learning promises. That would have allowed me to avoid many mistakes. Hopefully, these two posts will constitute a resolved promise of successful and happy coding for you:

readPosts().then(happyCoding);

Traps, anti-patterns and tips about AngularJS promises - Ninja Squad

Traps, anti-patterns and tips about AngularJS promises - Ninja Squad


AngularJS promises are not an easy concept to understand, and the documentation, although improving, could contain more examples on how to properly use them.

I’ve fallen in a few traps when using them, and have seen many trainees and StackOverflow users fall into them as well. This post gives examples of those traps and misuses, and how to better use promises.

You can’t escape from asynchronism
Most beginners, including myself, aren’t used to asynchronous programming. The first trap is to think you can escape from it, and transform an asynchronous call into a synchronous one. The reality quickly hits you in the face: that’s not possible.

Here’s some example of code I’ve seen numerous times:

app.controller('PoniesCtrl', function($scope, ponyService) {
$scope.ponies = ponyService.getPonies();
});
It absolutely makes sense: you want an array of ponies in the scope, so you get them from the service allowing to get ponies from the backend:

app.factory('ponyService', function($http) {
var getPonies = function() {
return $http.get('/api/ponies');
};

return {
getPonies: getPonies
};
});
Well, that won’t work. $http.get() is an asynchronous call. It doesn’t return ponies. It returns a promise. And this promise will be resolved when the HTTP response is available. Displaying a promise in the view used to work in the early versions of AngularJS, but it doesn’t anymore. The array itself must be in the scope.

So let’s fix the code:

var getPonies = function() {
$http.get('/api/ponies').success(function(data) {
return data;
});
};
Once again, that won’t work. Now getPonies() doesn’t return anything anymore. The return data statement is not returning from getPonies(). It’s returning from the anonymous callback function passed to success().

So let’s fix the code again:

var getPonies = function() {
var ponies;
$http.get('/api/ponies').success(function(data) {
ponies = data;
});
return ponies;
};
Nope. Still incorrect. The anonymous callback is not called immediately. It’s called when the response is available. And that happens long after getPonies() has returned. So the function still returns undefined.

All these errors come from the fact that we’re trying to transform an asynchronous call into a synchronous one. That is simply not possible. If it was, the AngularJS $http service wouldn’t bother us with promises and callbacks in the first place. So let’s accept this fact, and use a callback:

app.controller('PoniesCtrl', function($scope, ponyService) {
ponyService.getPonies(function(ponies) {
$scope.ponies = ponies;
});
});

app.factory('ponyService', function($http) {
var getPonies = function(callbackFn) {
$http.get('/api/ponies').success(function(data) {
callbackFn(data);
});
};

return {
getPonies: getPonies
};
});
Congrats! This finally works. Except when it doesn’t. What if the HTTP call fails? How can the controller know about it? We would need a second callback.

var getPonies = function(successCallbackFn, errorCallbackFn) {
$http.get('/api/ponies').success(function(data) {
successCallbackFn(data);
}).error(function(data) {
errorCallbackFn(data);
});
};
But wait: that’s exactly what promises allow. Passing two callbacks: one to handle a successful response, and one to handle an error response. And BTW, why wrap the callback functions into anonymous functions? We’re making our life more complex than it should be. We should simply return the HTTP promise, and let the controller deal with it.

app.controller('PoniesCtrl', function($scope, ponyService) {
ponyService.getPonies().success(function(data) {
$scope.ponies = data;
});
});

app.factory('ponyService', function($http) {
var getPonies = function() {
return $http.get('/api/ponies');
};

return {
getPonies: getPonies
};
});
See, our first implementation of the service was right, after all. We simply had to acknowledge the fact that an asynchronous service should return a promise.

The Real Thing - You To Me Are Everything
The previous version works fine, but it still has a design issue. The controller assumes that the service returns an HttpPromise. For some bizarre reason, AngularJS thought it would be a good idea if promises returned by the $http service were different than all the other $q promises: they would have additional success() and error() methods, that would basically do the same thing as then(), but in a different way:

the callback function would take 4 arguments (data, status, headers and config) instead of just one (the http response object)
success() and error() wouldn’t return a new promise as then() does (more on that later), but the original HttpPromise
I personally think that was a bad idea. Most asynchronous calls we do when learning AngularJS are $http calls, and we thus start learning special HTTP promises instead of learning the real thing, with the standard API that would also work for all the other promises.

Let’s rewrite our code without assuming the service returns an HTTP promise. The service could after all return a hard-coded list of ponies, or use websockets, for example. So let’s use the “classical” promise API.

app.controller('PoniesCtrl', function($scope, ponyService) {
ponyService.getPonies().then(function(data) {
$scope.ponies = data;
});
});
Oops. That won’t work. The actual value wrapped by the promise is not the array of ponies. It’s the HTTP response object, whose data contains the ponies. So let’s fix the code again:

app.controller('PoniesCtrl', function($scope, ponyService) {
ponyService.getPonies().then(function(response) {
$scope.ponies = response.data;
});
});
That will work fine. But once again, we’re assuming, in the controller, that the service returns an HttpPromise. Or at least a promise of something that looks like an HTTP response. Let’s change our service and make it return a promise of ponies, rather than a promise of HTTP response.

We’ll thus have to create our own promise, right? So we’ll have to use the $q service:

var getPonies = function() {
var defer = $q.defer();

$http.get('/api/ponies').then(function(response) {
defer.resolve(response.data);
});

return defer.promise;
};
Great. Now we return a promise of ponies. Except when the HTTP request fails. In that case, the returned promise will never be resolved nor rejected, and the caller won’t be aware of the error. So let’s fix it:

var getPonies = function() {
var defer = $q.defer();

$http.get('/api/ponies').then(function(response) {
defer.resolve(response.data);
}, function(response) {
defer.reject(response);
});

return defer.promise;
};
That is fine. But it’s way more complex than it should be. Let’s look at the documentation of $q, and especially at the documentation of the function then():

This method returns a new promise which is resolved or rejected via the return value of the successCallback, errorCallback.
Don’t know about you, but I need an example to understand this:

var getPonies = function() {
// then() returns a new promise. We return that new promise.
// that new promise is resolved via response.data, i.e. the ponies

return $http.get('/api/ponies').then(function(response) {
return response.data;
});
};
Now that’s cool. We can “transform” a promise of response into a promise of ponies by transforming the response into ponies in the then() callback. Note that if the HTTP response fails, the returned promise of ponies will be rejected as well, and the controller will thus be aware of the error if it wants to:

app.controller('PoniesCtrl', function($scope, ponyService) {
ponyService.getPonies().then(function(data) {
$scope.ponies = data;
}).catch(function() {
$scope.error = 'unable to get the ponies';
});
});
Have you noticed? You can simply pass it a success callback, and then chain with a call to catch() which only takes an error callback. I find that style more readable than the standard style of passing two callbacks to then():

app.controller('PoniesCtrl', function($scope, ponyService) {
ponyService.getPonies().then(function(data) {
$scope.ponies = data;
}, function() {
$scope.error = 'unable to get the ponies';
});
});
Beware that it’s not strictly equivalent, though, as catch() is called on the promise returned by then(), and not on the original promise.

This post is getting long. The next one will talk about promise chaining, resolving and rejecting, and unit tests. Stay tuned!