Wednesday, February 4, 2015

I Promise

Not This Kind of Promise
I've been working with Node for a while now. I wrote the whole application for my web comic using it as a way to learn something new and make something real at the same time. I like to be practical like that. One thing that I've noticed, as I'm sure anyone who's worked with JavaScript for more than 20 minutes has, is the tendency for large numbers of nested callbacks to create the Pyramid of Doom.

Sounds like you need to carry a whip and wear a fedora if you're going in there.

No, it's really just code like this. It grows sideways faster than it grows down.

call1(function(data) {
    call2(data, function(moreData) {
        call3(moreData, function(evenMoreData) {
            call4(evenMoreData, function(finalData) {
                // finally got what I wanted
            });
        });
    });
});

There is an alternative to this approach, called Promises. I just recently started replacing some of my callback-style code with Promises to see how it works.

A Promise, according to the Promises/A+ spec, is a representation of the eventual result of an asynchronous operation. The primary way of interacting with a promise is through its then method - where callbacks are registered to receive the eventual result of the operation or the reason why it failed.

Yeah, I had no idea what that meant the first time I read it either. I learn by experimenting. So I looked at some examples and then tried to convert one of my callback-style functions to use a Promise instead. Here's what happened.

First, I had to get a Promises library. Promises aren't supported natively in Node, so I used the Q module. It implements the Promises spec.

Here's the first function that I modified to use Promises, before I modified it.

function listImages(cb) {

    db.query('SELECT filename, type FROM comic_img', function(err, rows) {
        if (err) {
            cb(err);
        } else {
            var data = [];
            if (rows.length > 0) {
                for (var i = 0; i < rows.length; i++) {
                    data.push({
                        filename: rows[i].filename,
                        type: rows[i].type
                    });
                }
            }
            cb(null, data);
        }
    });

}

It's just one of many functions in my comic's data access module, the one that returns the list of all available images that can be used in a comic. As you can see, it implemented a callback model, where a function to be called (the callback) was supplied as the argument. The database is queried, and the resulting data is returned to the callback. As is typical for callbacks, the first argument sent to the callback function is any error that may have occurred or null if everything worked. The second argument will be the actual data which the caller was requesting.

Calling the listImages function in my web application originally looked something like this:

listImages(function (err, data) {
    if (err) {
        next(err);
    } else if (data) {
        res.setHeader('Content-Type', 'application/json');
        res.send(data);
    }
});

Now let's look at the function after I converted it to use Promises instead of callbacks.

var q = require('q');
function listImages() {

    var deferred = q.defer();

    db.query('SELECT filename, type FROM comic_img', function(err, rows) {

        if (err) {

            deferred.reject({
                message: 'database query failed',
                error: err
            });

        } else {

            var data = [];
            if (rows.length > 0) {
                for (var i = 0; i < rows.length; i++) {
                    data.push({
                        filename: rows[i].filename,
                        type: rows[i].type
                    });
                }
            }

            deferred.resolve(data);

        }
    });

    return deferred.promise;

}

The differences are not that extreme. On line 1, I have to require the Q module of course. Internally the function still makes the call to the database the same way, but it doesn't require a callback function to be passed in as an argument. Instead it creates a deferred object there on line 4 and returns the deferred.promise at the end. That's different - the original version didn't return anything. If the database query fails, I call deferred.reject and pass in an object describing the error (line 10). If everything works, I call deferred.resolve and pass in the data. But the database call happens asynchronously - whoever called this function got the promise returned to them and has to use that for handling the eventuality of data or failure. So here's how the caller changed:

listImages().then(function(data) {
    res.setHeader('Content-Type', 'application/json');
    res.send(data);
}, function(error) {
    next(error.error);
});

Calling listImages now returns a Promise, and the primary way of interacting with Promises is through the then method. The first parameter to the then function is a function to be called when the Promise is resolved, taking the data that was produced (what I passed to the deferred.resolve function above). The second parameter is a function to be called if the Promise is rejected and taking an object representing the reason for rejection (the error description object I passed to deferred.reject).

So there is my simple example of using Promises instead of the callback pattern to deal with asynchronous operations. There is a lot more that the Promises specification offers and much more you can do with the Q library. I'm going to keep working with it see what happens. I might post more later. In the mean time, you can read this comic about frogs in the Silicon (Dioxide) Valley.

Amphibian.com comic for 4 February 2015

No comments:

Post a Comment