from http://12devs.co.uk/articles/promises-an-alternative-way-to-approach-asynchronous-javascript/
Even if you’ve done very little JavaScript, you should be
familiar with callbacks, which are used heavily for managing
asynchronous operations in JavaScript. We’re going to look at an
alternative way to handle such asychronous code by using Promises. The
examples I’m going to cover are going to use Node.js, but the techniques
and libraries involved work equally well for client side JavaScript
too. And, you definitely don’t need be an expert on Node.js to follow
this article.
Why (not) callbacks?
Let’s get started with a simple example of reading the contents of a file in Node.js:
// readFileExample.js
var FS = require('fs');
FS.readFile('file.txt', 'utf8', function(err, data) {
if (err) throw err;
console.log('File has been read:', data);
});
console.log('After readFile.');
Here we give
readFile
a function to call (which we refer
to as a “callback”). This callback function is invoked once the
contents of the file has been read. This is an asynchronous operation
because the
readFile
call is non-blocking and if you were to execute the above program, you will notice that
After readFile.
will be printed before
File has been read
.
$ node readFileExample.js
After readFile.
File has been read.
There is nothing wrong about the use of a callback in this example.
It’s readable, simple and gets the job done. However, managing callbacks
tends to get tricky when you want to do something more complex. I’m
going to extend the above example to show what I mean. Let’s say we need
to send the file’s data to a couple of web services
concurrently and then show the results after both calls finish. Here’s how that will look:
var FS = require('fs'),
request = require('request');
function getResults(pathToFile, callback) {
FS.readFile(pathToFile, 'utf8', function(err, data) {
if (err) return callback(err);
var response1, response2;
request.post('http://service1.example.com?data=' + data), function(err, response, body) {
if(err) return callback(err);
response1 = response;
next();
});
request.post('http://service2.example.com?data=' + data), function(err, response, body) {
if(err) return callback(err);
response2 = response;
next();
});
function next(){
if(response1 && response2){
callback(null, [response1, response2]);
}
}
});
}
Do you see what I mean? We had to define a couple of variables (
response1
and
response2
) to track the state of the two requests. The callbacks to both the service calls have to call the
next()
function, which will then check the state of the requests and print the
combined results if both requests are done. The above code is not even
complete! For instance, notice that we will be invoking the callback
twice if both the service calls fail.
Callbacks also lead to another problem, which you should be already
familiar with: callback hell. When you have to perform a number of
actions in a specific sequence, nesting your callbacks leads to code
like this:
asyncCall(function(err, data1){
if(err) return callback(err);
anotherAsyncCall(function(err2, data2){
if(err2) return calllback(err2);
oneMoreAsyncCall(function(err3, data3){
if(err3) return callback(err3);
// are we done yet?
});
});
});
Let’s try that again with Promises
There are ways to make those callbacks look prettier, but that’s not
the point of this article. Instead, I want to show you an alternate way
of handling such tricky asynchronous operations, through the use of
Promises.
A Promise is a placeholder object that represents the result of an
async operation. This object will hold the information about the status
of the async operation and will notify us when the async operation
succeeds or fails. Enough theory, let’s see re-write the nested callback
example above using Promises.
asyncCall()
.then(function(data1){
// do something...
return anotherAsyncCall();
})
.then(function(data2){
// do something...
return oneMoreAsyncCall();
})
.then(function(data3){
// the third and final async response
})
.fail(function(err) {
// handle any error resulting from any of the above calls
})
.done();
The
asyncCall()
, instead of requiring a callback, returns us a Promise object. The subsequent
then()
calls on the Promise object also return promises, thus allowing us to chain a sequence of asynchronous operations. The
fail()
takes a function that will be invoked when any of the preceding
asynchronous calls fail. Since an error automatically cascades down to a
separate handler, we don’t have to check for them in each stage, like
we have to do in the callback-based approach. As you can see, this type
of chaining “flattens” the code and drastically improves readability.
Q – a neat library for using Promises
With that rather large introduction to Promises out of the way, let’s
look at some practical ways of using Promises when working with
Node.js. The first challenge you will face in adopting promises is that
all of Node’s core libraries and most of the user-land libraries work
using callbacks. Thankfully, there are some awesome libraries that allow
us to convert these callback-based APIs to promises. I’m going to use
the
Q promise library to show how to get going with Promises in Node.js.
Let’s go back to the very first file reading example we saw and re-write that using Q:
// readFileUsingPromises.js
var FS = require('fs'),
Q = require('q');
Q.nfcall(FS.readFile, "file.txt", "utf-8")
.then(function(data) {
console.log('File has been read:', data);
})
.fail(function(err) {
console.error('Error received:', err);
})
.done();
Q has a handy
nfcall()
utility function that converts
‘readFile()’ to a promise. With this simple wrapper, you can start using
Promises even if a library uses callbacks. Go ahead and try executing
the above code (make sure you have the Q module installed by running
npm install q
first).
Q also makes it very easy to run async operations concurrently. Let’s
re-write the earlier example involving service calls using Q. This
time, I’m going to choose a real-world API, so you can try executing
this code on your own. A file contains the name of a Github repository
in a single line, like this:
// repos.txt
joyent/github
We want to read the name of a repository from a file and then call
Github’s APIs to get the information about the repository’s
collaborators and also fetch the latest commits from the repository.
Here’s how that will look:
var FS = require('fs'),
Q = require('q'),
request = require('request');
function getResults(pathToFile) {
return Q.nfcall(FS.readFile, pathToFile, "utf-8")
.then(function(repo) {
var options = { headers: {'User-Agent': 'MyAgent'} }; // github requires user agent string
return [Q.nfcall(request, 'https://api.github.com/repos/'+repo+'/collaborators', options),
Q.nfcall(request, 'https://api.github.com/repos/'+repo+'/commits', options)];
})
.spread(function(collaboratorsRes, commitsRes) {
return [collaboratorsRes[1], commitsRes[1]]; // return the response body
})
.fail(function(err) {
console.error(err)
return err;
});
}
// actual call
getResults('repos.txt').then(function(responses) {
// do something with the responses
});
This example builds on top of the basic constructs of Promises that we have already seen, except for the use of the
spread
function. Notice how we make 2 concurrent API calls for fetching the
collaborators and commits independently. These two calls are returned as
an array of promises, which are then “spread” over their eventually
fulfilled results. Slick isn’t it? We don’t have to track the state of
the individual requests anymore, as we did with the callback based
version.
What’s next?
With that, we have covered the basics of Promises and hopefully you
got a glimpse of the powerful abstractions they provide us. You should
definitely consider using them whenever you’re having trouble managing
multiple, inter-dependent asynchronous calls. You will find that
Promises definitely makes asynchronous operations easier to reason
about. If you want to read more about Q,
this guide will be really helpful.