ES6 Style Promises in Angular 1.3
We mainly took a look at completely new features that come with the Angular 1.3 release until now. Things like ngModelOptions, Angular-hint or One-time Bindings are not just minor improvements, but rather real extensions to the framework. However, there have not only been significant new features added to the release, but also a ton of bug fixes and nice little additions that we might have overlooked. One of them is the ES6 streamlined promise API and today we gonna take a look what it brings to the table.
Asynchronous worlds with Promises
In order to understand what the new streamlined addition to the existing promise API means, we first have to make sure we’re all on the same page and know what promises are and how they’ve been implemented in Angular before. We don’t want to go in too much detail here though, since there are a ton of resources in the interwebs, but let’s take a very quick look at promises and move on then.
In just one sentence, a promise is an object that is used for deferred and asynchronous computations. So what does that mean? Well, in JavaScript we can have asynchronous code execution with, for example, callbacks. And with these things we’re able to execute some code once another execution that ran before is done without blocking the actual code execution context. We call this asynchronous.
Here’s an example:
onceDone(function () {
// do something once `onceDone()` calls you
});
Here we have a function onceDone()
that expects a function that is executed, once the onceDone()
function is done with its work and it doesn’t block the rest of the code that might be there.
Okay, that’s clear. But where and how come promises into play? There’s a scenario that JavaScript developers love to call ”callback hell”. Callback hell is something that we have, when we nest a lot of functions in JavaScript that are asynchronous and have callbacks to be executed. Just take a look at the following code snippet.
onceDone(function (files) {
files.forEach(function (filename, fileIndex) {
filename.size(function (err, values) {
values.width.forEach(function (value) {
// ... and so on
});
});
});
});
You get the idea right? While it is very common to have function calls that get a function callback, it can lead to very hard to get right code when we have to nest a lot of these. I recommend to head over to callbackhell.com to get a better picture, if things are still unclear.
We can get around this issue by defining named functions first, and pass just these as callbacks instead of using anonymous functions all the time, but it’s still not a very handy way to handle asynchronous JavaScript. And this is where promises come in.
Promises are a software abstraction or proxies, that make working with asynchronous operations much more pleasant. Coming back to our onceDone()
function example, here’s what the code would look like if onceDone()
used promises.
var promise = onceDone();
promise.then(function () {
// do something once `onceDone` calls you
});
Looks very similar right? But there’s a huge difference. As you can see, onceDone()
returns something that is a promise which we can treat as first-class objects. This promise holds the actual state of the asynchronous code that has been called, which can be fulfilled
, rejected
or pending
. We can pass promises around and even aggregating them. That’s a whole different way of handling our asynchronous code.
What we also see, is that the promise has a method .then()
. This method expects two parameters which are functions of which one gets called when the asynchronous execution was fulfilled and the other one when it was rejected.
var promise = functionThatReturnsAPromise();
promise.then(fulfilledHandler, rejectedHandler);
As I mentioned, we can aggregate them. .then()
also returns a promise that resolves with the return value of the executed handler (fulfilled
or rejected
). That enables us to chain promises like this:
var promise = functionThatReturnsAPromise();
promise
.then(fulfilledHandler, rejectedHandler)
.then(doSomethingElse)
.then(doEvenMore)
.then(doThis);
Compare this with our callback hell code snippet and you know why promises are so powerful. In fact, there’s a lot more about promises to tell, but that’s out of the scope of this article. If you want to read more about promises in general, I recommend reading Domenic’s article and the MDN docs on promises. But let’s get back to promises in Angular.
Promises in Angular and $q
Angular comes with a promise implementation by default. It has a $q service that we can of course inject and use though-out our application. Angular’s implementation is highly inspired by Kris Kowal’s Q library which is an implementation of the Promises/A spec.
It comes with a Deferred API
which lets you get instances of deferred objects that hold a promise object. We can use the API of a deferred to either resolve or reject a promise depending on what our code should do. Here’s a quick example:
// `$q` is injected before
var deferred = $q.defer();
anAsyncFunction(function (success) {
deferred.resolve(success);
}, function (error) {
deferred.reject(error);
});
var promise = deferred.promise;
// and later
promise.then(function () {
// do something when `anAsyncFunction` fulfilled
});
We have a function anAsyncFunction()
which is asynchronous and we use the deferred API to get a promise out of it. One thing to notice here is that our function doesn’t know anything about promises but we use the deferred API to get a promise back. The deferred API comes with a few more features that I don’t want to detail here, but you can read about them in the official docs.
Have you ever used Angular’s $http
service? Sure you did. And as we know, XMLHttpRequests are asynchronous too. Guess what $http
service uses to expose its .success()
and .error()
APIs? Right. Promises. When making XHR calls with $http
we’re also using $q
implicitly. It just adds some sugar APIs to the promise it returns:
$http.get('some/restful/endpoint')
.success(function (data) {
// do something with `data`
})
.error(function (reason) {
// oups, something went wrong
});
In fact, we can use the promise native APIs to achieve the same:
$http.get('some/restful/endpoint')
.then(function (data) {
// do something with `data`
}, function (reason) {
// oups, something went wrong
});
Okay, so we now got a picture of promises in Angular. There’s also a nice talk by the awesome Dave on promises at ngEurope, I recommend checking that one out too. But what is it with the ES6 style promises that we’ve mentioned in the blog title?
ES6 style Promises in Angular 1.3
Although it’s nice to have the deferred API in Angular to deal with promises, it turns out that the ECMAScript standard defines a slight different API. Taking a look at the MDN docs on Promises, we see that Promise
is a constructor in ES6 that takes an executor
function that has access to a resolve
and a reject
function to resolve and reject promises respectively.
Angular 1.3 streamlined its promise APIs partly with the ES6 standard. I say partly here, because not all methods are supported yet. However, what the team has streamlined is that $q
can also be used as a constructor now.
So instead of doing creating a deferred like this:
function myFunctionThatReturnsAPromise() {
var deferred = $q.defer();
anAsyncFunction(function (success) {
deferred.resolve(success);
}, function (error) {
deferred.reject(error);
});
return deferred.promise;
}
myFunctionThatReturnsAPromise().then(yay, nay);
We can now use the promise constructor API and return it directly without creating a deferred object first:
function myFunctionThatReturnsAPromise() {
return $q(function (resolve, reject) {
anAsyncFunction(function (success) {
resolve(success);
}, function (error) {
reject(error);
});
});
}
Even if this is just an optical difference at a first glance, it’s nice to know that we can safe the lines of code to create a deferred first. Also, the fact that the $q
API is now closer to the actual spec makes the code more reusable in the future.
Now we might wonder, if we have to change all of our code where we’ve used $q.defer()
to work with promises. The answer is no. As mentioned at the beginning of the article, this is a nice small addition (rather than a new feature or replacement) in the 1.3 release that doesn’t break the code.