Taking Toddler Steps with Node.js – The Towering Inferno Revisited

August 15th, 2013

Soon after I started using Node.js, I ran into the phenomenon of multiple nested callbacks that create some kind of horizontal tower effect. The solution I came up with in order to improve the readability of my code was using a library called step, as described in this blog post that I wrote at that time.

Over the past years I switched over to a couple of other control flow libraries that solve the same problem as step, but  eventually I settled on using the async library.

Let’s look back at the problem I used in my original blog post:

image

Here’s the slightly refactored equivalent using async:

http.createServer(function(request, response) {
    async.waterfall([
        assembleFilePath,
        readFavoritePodcastsFromFile,
        addNewFavoritePodcastToFile
        ], 
        function(error, favoritePodcasts) {
            if(error)
                return response.end(error);

            response.writeHead(200, {
                'Content-Type': 'text/html', 
                'Content-Length': favoritePodcasts.length
            });

            response.end(favoritePodcasts); 
        }
    );
})
.listen(2000);

function assembleFilePath(callback) {
    var filePath = path.join(__dirname, 'podcasts.txt');
    callback(null, filePath);
}

function readFavoritePodcastsFromFile(podcastsFilePath, callback) {
    fileSystem.readFile(podcastsFilePath, 'utf8', function(error, data) {
        if(error)
            return callback(error);

        callback(null, podcastsFilePath, data);
    });                     
}

function addNewFavoritePodcastToFile(podcastsFilePath, favoritePodcastData, callback) {
    var favoritePodcasts = favoritePodcastData;

    if(-1 == favoritePodcasts.indexOf('Astronomy Podcast')) {
        favoritePodcasts = favoritePodcasts + '\n' + 'Astronomy Podcast';       
        fileSystem.writeFile(podcastsFilePath, favoritePodcasts, function(error) {
            if(error)
                return callback(error);

            callback(null, favoritePodcasts);
        });                     
    }
    else {
        process.nextTick(function() {
            callback(null, favoritePodcasts);
        });     
    }
}

 

Here I’ve used the waterfall method of the async library in order to pass results from one function to the next. Other functions that I often use are series and parallel. Notice that in the addNewFavoritePodcastToFile function I used process.nextTick instead of just invoking the callback. This is done in order to prevent inconsistent behavior of the function. I also wrote about this in the past.

There has been a lot of buzz lately around promises, so I decided to drink some of this kool-aid. Basically, we can achieve the same kind of solution as with the async library.

http.createServer(function(request, response) {

    assembleFilePath()
    .then(readFavoritePodcastsFromFile)
    .then(addNewFavoritePodcastToFile)
    .then(function(favoritePodcasts) {
        response.writeHead(200, {
            'Content-Type': 'text/html', 
            'Content-Length': favoritePodcasts.length
        });

        response.end(favoritePodcasts); 
    })
    .done();
})
.listen(2000);

function assembleFilePath() {
    return Q.fcall(function() {
        return path.join(__dirname, 'podcasts.txt');
    });
}

function readFavoritePodcastsFromFile(podcastsFilePath) {
    var deferred = Q.defer();

    fileSystem.readFile(podcastsFilePath, 'utf8', function(error, favoritePodcasts) {
        if(error)
            return deferred.reject(new Error(error));

        deferred.resolve({
            favoritePodcasts: favoritePodcasts,
            podcastsFilePath: podcastsFilePath
        });
    });

    return deferred.promise;
}

function addNewFavoritePodcastToFile(data) {
    var deferred = Q.defer(),
    favoritePodcasts = data.favoritePodcasts;

    if(-1 == favoritePodcasts.indexOf('Astronomy Podcast')) {
        favoritePodcasts = favoritePodcasts + '\n' + 'Astronomy Podcast';       
        fileSystem.writeFile(data.podcastsFilePath, favoritePodcasts, 
        function(error) {
            if(error)
                return deferred.reject(new Error(error));

            deferred.resolve(favoritePodcasts);
        });                     
    }
    else {
        process.nextTick(function() {
            deferred.resolve(favoritePodcasts);
        });
    }

    return deferred.promise;
}

 

I’ve used the Q library for this code sample. For an excellent introduction to promises and the Q library, check out this great article on the StrongLoop blog. I think the approach using promises looks, uhm … promising as well.

Are you, dear reader, using a control flow library, which one and why?

Until next time.

  • Chris Tavares

    Have you looked at https://github.com/Sage/streamlinejs? It’s a js precompiler that essentially gives you async/await style programming in Javascript. Pretty cool.

    • JanVanRyswyck

      This has been on my list for a while, but I think I’ll have to revisit it. Thanks for pointing this out.