Promises in JavaScript with rsvp

Wednesday, May 7th 2014

JavaScript with its single-threaded execution encourages the use of asynchronous programming and callbacks that can lead to some really tough-to-manage code. The use of promises can radically simplify the structure of our asynchronous JavaScript code which leads to more comprehensible code. This post talks about the use of promises in JavaScript, specifically with the rsvp library.

In case you’re interested, our good friend Bryan Ray has his own JavaScript-oriented post in reference to his awesome side project Huddle, a “node-based chat application that composes many useful features for helping small teams work more effectively.”

The problem

I’m going to use an example for node.js; however, you can apply the same techniques for browser-based code for your favorite browser[1].

The following example loads a file, parses the JSON in it, and uses nano to save each “record” to an instance of a CouchDB.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var fs, nano, db, path, filepath;
fs = require('fs');
nano = require('nano');
path = require('path');
db = nano('http://localhost:5984/callback_example');
filepath = path.join(__dirname, 'data.json');

function uploadToCouch(list, e) {
if (e) {
return console.error(e);
}
if (list.length === 0) {
return;
}
db.insert(list[0], uploadToCouch.bind(null, list.slice(1)));
}

fs.readFile(filepath, 'utf8', function (e, data) {
var records;
if (e) {
return console.error(e);
}
records = JSON.parse(data);
uploadToCouch(records);
});

The first six lines just set up the stuff we need: boring.

Line 8 declares the function uploadToCouch that I use to actually insert the document into the database. The list parameter contains the list of things to insert into the database. The e parameter contains an error from the last upload, if any.

Line 9 checks for an error and quits if it finds one.

Line 12 terminates the uploads since the upload list is empty.

Line 15 inserts the first item in the list into CouchDB and sets the callback for the insert function to uploadToCouch with the “tail” of the list, that is, the list with the first item removed since that’s the thing we’re uploading into CouchDB in that call.

You may have not used (or even seen) the bind method available in JavaScript 5.1 and newer. The line

1
uploadToCouch.bind(null, list.slice(1))

is the same as this

1
2
3
function (list) {
uploadToCouch(list.slice(1));
}

Anyway, if you don’t program with functional languages that promote tail recursion modulo cons, then the code in the block above may make no sense to you and many others. We need a better way to express this.

Using an asynchronous utility library

Now, I’m going to rewrite the previous code block using an asynchronous utility library. I have chosen async for this example; however, you can find a lot of others out there, too.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var fs, nano, db, path, filepath, async;
fs = require('fs');
nano = require('nano');
path = require('path');
async = require('async');
db = nano('http://localhost:5984/callback_example');
filepath = path.join(__dirname, 'data.json');

fs.readFile(filepath, 'utf8', function (e, data) {
var records, inserts;
if (e) {
return console.error(e);
}
inserts = [];
records = JSON.parse(data);
records.forEach(function (record) {
inserts.push(function (callback) {
db.insert(record, callback);
});
});
async.parallel(inserts, function (e) {
if (e) {
console.error(e);
}
});
});

This seems much simpler and easier to understand, in my opinion.

Line 16 marks the first real deviation from the last section where the code iterates over each of the records found in the file and pushes a function into the inserts array.

Line 21 uses the async library to invoke all of the insert functions in parallel and, should something bad happen, prints out the error statement from the offending insert.

Promises, though, offer us something even more comprehensible.

Using promises

Before we jump into the actual example with promises, let’s look at the way the community has decided to express promises with JavaScript.

Promises/A+

From the Promises/A+ Web site:

A promise represents the eventual result of an asynchronous operation. The primary way of interacting with a promise is through its then method, which registers callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled.

In short, a promise is an object that has a then method that takes two parameters of type Function. The promise should invoke the function in the first parameter if everything goes well. The promise should invoke the second function if something failed.

Here’s a practical example using rsvp:

1
2
3
4
5
6
7
8
9
10
11
12
var fs, rsvp;
fs = require('fs'),
rsvp = require('rsvp');

new rsvp.Promise(function (resolve, reject) {
fs.readFile('my_file', 'utf8', function (error, data) {
if (error) {
return reject(error);
}
resolve(data);
});
});

The promise here accepts a function with the resolve and reject parameters. When the code successfully reads the file, it “resolves” the promise with the data read in from the file. If the read operation fails, then the code “rejects” the promise and provides the error as the reason for that rejection.

Here’s the cool thing: if your code resolves a promise with another promise, the promise specification that the promise library must evaluate that promise, too! This means that the promise specification has chaining of promises built in. We can use that to our benefit!

The example with promises

Here I go with the rsvp library to do the same thing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var fs, nano, db, path, filepath, rsvp, insert;
fs = require('fs');
nano = require('nano');
path = require('path');
rsvp = require('rsvp');
db = nano('http://localhost:5984/callback_example');
filepath = path.join(__dirname, 'data.json');
insert = rsvp.denodeify(db.insert);

fs.readFile(filepath, 'utf8', function (e, data) {
var records, promise;
if (e) {
return console.error(e);
}
promise = new rsvp.Promise(function (yes) { yes(); });
records = JSON.parse(data);
records.forEach(function (record) {
promise = promise.then(insert.bind(null, record));
});
promise.catch(function (e) {
console.error('error:', e);
});
});

On line 8, you can see a call to rsvp.denodeify. This takes a function with a normal asynchronous callback signature of function ([data], function callback(error, result)) and turns it into a promise. This call to denodeify turns the insert method of nano into a promsie for later on.

Line 15 creates a “default promise” that resolves rather than rejects. That will put the promise into a state of “continue”.

Line 18 shows that for each record, we just chain a call to insert for the record onto the promise.

Line 20 catches any error from the promise chain’s execution and just prints it out to standard error.

I think this is the easiest to understand of the three. No tail recursion, no array of function invocations, just repeated calls to then. I really like that.

Where does that leave us?

Now that you understand promises (or have started on that journey), I can now write about leslie-mvp. It turns out that these promises make it really easy to write complex frameworks in a straight-forward and easy-to-understand way. Take a look at the source code and you’ll see promises everywhere. They provide the backbone of the evaluation of the entire MVP stack!

More on that in a future post, though!


  1. 1.Unless your favorite browser is something stupid like ie6. Then, I ain't got nothing for you.