I promise you: Flat you shall be

Philipp Burgmer
theCodeCampus Knowledge
4 min readOct 28, 2016

Code Reviews are one of our main tasks at theCodeCampus, especially for Angular applications. And if there is one thing standing out: a lot of people have problems with promises, chaining them in particular. Promises are highly present in AngularJS 1 applications. But this subject is not limited to AngularJS. The same applies to standard ES6 promises. The details differ a little bit but the problem is the same.

So we should take some time to take a look at the API and how to avoid a common pitfall: flattening promises.

Here comes a short recap about promise basics first. How to create them and use the then-function. The second part of this blog post is about chaining promises and a detail lots of developers fail to see: we can return a promise from a handler.

If you’re familiar with promise basics, feel free to scroll down to the headline Chaining and flattening Promises.

Promise Basics

In AngularJS 1 there are two different APIs for creating promises: the deferred API and the promise constructor. Both create a promise instance with the same functions.

Create a promise with the Deferred API

To create a promise with the deferred API you have to create a deferred object first and then get the promise out of this deferred object. The deferred object is your API to resolve or reject the corresponding promise.

var deferred = $q.defer();
// deferred.resolve(value) or deferred.reject(reason)
return deferred.promise;

Main advantage and main source of error at the same time is the reference to the deferred object. You can store it and as with any reference you can share it, pass it around.

Create a promise with the Promise Constructor

Starting with AngularJS 1.3, the preferred way to create a promise is to call $q as a constructor. To be exact: in AngularJS 1 $q is not a constructor but a factory. You call it to create a new instance but without new in front of it. After all it is pretty identical to the ES6 Promise Constructor.

return $q(function(resolve, reject) {
// resolve(value);
// or
// reject(reason);
});

Main difference to the Deferred API: you don’t have a reference to the resolver/rejector outside the function you pass to the constructor. ES6 code would look like this:

return new Promise(function(resolve, reject) {
// resolve(value);
// or
// reject(reason);
});

Promise Handler

For the sake of simplicity we focus on success handler here and ignore the error handler here. The Promise API is actually quite small: then, catch and finally where catch and finally are shortcuts only.

promise.then(function(result) {
// do something with the result
});

You can pass a success handler to the promise’s then function. This handler will alway be called asynchronously, no matter if the promise is already resolved/rejected or not. And it will be called zero or at most one time: zero if the promise never gets resolve/rejected and exactly one time if the promise gets or already is resolve/rejected.

Chaining Promises

The Promise API is not very expressive, unfortunately. then is more than just a function to register handlers. It is a functional mapper. You may know functional mapping from arrays.

var powers = [1,2,3].map(function(x) {
return x * x;
});

With then, we can do something similar. But unfortunately the name then does not tell us. then called on promise A immediately returns a new promise (B). This promise (B) gets resolve with the return value of our handler.

var promiseB = promiseA.then(function(x) {
return x * x;
});
promiseB.then(function (x) { console.log(x); });

So if promiseA will be resolved with 3, promiseB will then be resolved with the return value of the handler passed to promiseA.then, which will be 9. You got it? Promise#then is similar to Array#map with two differences: Array#map is synchronous and operates on multiple values where Promise#then is asynchronous and operates on a single value.

Chaining and flattening Promises

Now to the main topic of this post. What happens if we have a promise delivering an id. We use this id to ask a service for an object. But the service also returns a promise also. This would lead us to a promise of a promise of an object or to use the TypeScript generics syntax: Promise<Promise<Object>> instead of Promise<Object>.

To handle this lots of developers create something like this:

var deferredForObject = $q.defer();var idPromise = getId();idPromise.then(function(id) {
service.getObjectById(id).then(function(object) {
deferredForObject.resolve(object);
});
});
return deferredForObject.promise;

You can do it with the Constructor API as well.

return $q(function(resolve) {
var idPromise = getId();
idPromise.then(function(id) {
service.getObjectById(id).then(function(object) {
resolve(object);
});
});
});

Believe me, I saw more than one pice of code doing this. But it could be so much easier and much more readable. Fortunately then has another hidden feature: it allows us to return a promise and it automatically extract the nested promise. In functional programming this is known as flattening. So the result of this

var idPromise = getId();var derivedPromise = idPromise.then(function handler(id) {
var objectPromise = service.getObjectById(id);
return objectPromise;
});
return derivedPromise;

is not Promise<Promise<Object>> but simply Promise<Object>. Nice flat code without the pyramid of doom.

Once again: If we return a value from a handler, the derived promise will be resolved with this value immediately. But if we return a promise from an handler, the derived promise will be coupled to this returned promise (objectPromise here) and will be resolved with its value, as soon as it’s available. Think about it as if derivedPromise get replaced by objectPromise. (which technically is wrong, but maybe it helps …)

Angular 2

In Angular 2 promises do not play an important role. In Angular 2 everything is an Observable. They have a much more expressive API than promises. And the have a different semantic. How to solve the described problem with observables will be part of an upcoming blog post. Stay tuned ;)

--

--

Philipp Burgmer
theCodeCampus Knowledge

Trainer and consultant at theCodeCampus (brand of W11K). We offer Angular and TypeScript trainings in Germany.