Writing better JavaScript with Promises

For the majority of my career, I have built web applications using the Microsoft .NET technology stack. Recently, I've been working on more and more projects developed in Node. As I continue to grow this skill-set, two things become more and more apparent:

  1. I'm much more productive in JavaScript than I ever dreamed possible. I mainly attribute this to the combination of a simple, elegant language, rock-solid tooling via the NPM ecosystem, and the vibrant, friendly community. I find myself beginning to favor the Node toolset over .NET for many small to mid-size applications. If you hopped in your DeLorean, tuned the dials back to 2014, cranked it up to 88 MPH and showed Blake from the past this blog post, there is no way he would believe you.

  2. While I do love the "new car smell" of working with Node, I often find myself missing features from a more mature C# .NET environment. Take for example my current pet peeve: callbacks.

Callbacks

To truly understand this "gripe", we need to take a step back. Node was built around a form of asynchronous programming similar to Continuation-passing Style (CPS). When a function needs to perform asynchronous IO operations, it will not block to return a result. Instead the function will take an additional "callback" argument. This callback is simply a function that will be called with the result of the asynchronous operation. This enables Node applications to be extremely efficient with CPU cycles by minimizing blocking threads.

Here's a simple example, using an inline anonymous function as the callback:

var fs = require("fs");
    
    fs.readFile("someFile.txt", function(err, data) {
      if (err) throw err;
      console.log(data);
    });
    

Here, the readFile method is asynchronous. It doesn't "return" a value. It will open a file, read the contents and then upon success or failure, pass the results to the anonymous function. This particular example shouldn't be too difficult to comprehend. But like most things in the programming: it gets much more difficult in the real world.

Callback Hell

Let's build upon this example, and say we want to read the contents of a file, save the contents to another file, and then log a message to the console:

var fs = require("fs");
    
    fs.readFile("firstFile.txt", func(err, firstFileData) {
      if (err) throw err;
      fs.writeFile("secondFile.txt", firstFileData, func(err, secondFileData) {
        if (err) throw err;
        console.log("Done!");
        });
      });
    

Ok, so still not too difficult to grasp what's going on with this example. Now, imagine that instead of logging a message to the console, we wanted to invoke another asynchronous operation which requires a callback, etc. I think you are beginning to see my point: We are on a dangerous path leading straight to an unsightly, nested code minefield. This is often referred to as "Pyramid of Doom", or "Callback Hell".

I heard ya like callbacks

How do we deal with Callback Hell?

One simply way is to favor named functions, or function variables over anonymous, in-line functions. Let's see this in action:

var fs = require("fs");
    
    var saveFile = function(dest, data, cb) {
      if (err) throw;
      fs.writeFile(dest, data, function(err) {
        if (err) throw err;
        cb();
        });
    }
    
    var copyFile = function(src, dest, cb) {
      if (err) throw;
      fs.readFile(src, function(err, data) {
        if (err) throw err;
        saveFile(dest, data, cb);
        });
    }
    
    var logDone = function() {
      console.log("Done!")
    }
    
    copyFile("firstFile.txt", "secondFile.txt", logDone);
    

As you can see, this does help with nesting, but still feels somewhat icky. We can do better!

An even better way!

As you can probably guess, the title of this post hints at another solution: Promises. Promises are essentially data structures that represent the result of an asynchronous operation. They can have one of the following states: pending, fulfilled, or rejected. Once a promise has been fulfilled or rejected, it is immutable.

The really nice thing about promises is that they can be chained together to create much more maintainable control-flow code. Let's restructure our example using the stellar bluebird promise library:

var Promise = require("bluebird");
    
    var fs = Promise.promisifyAll(require("fs"));
    
    function copyFile(src, dest) {  
      return fs.readFileAsync(src)  
                .then(writeFileAsync.bind(fs, dest));  
    }
    
    copyFile("firstFile.txt", "secondFile.txt");
        .then(function() { console.log("Done!") })
        .catch(function(e) { console.log(e); });
    

This looks much better to me, and as a bonus, I can now chain additional promises to the result of the copyFile method. The "catch" will handle any exceptions in the chain - which is extremely powerful!

In conclusion

I've shown you some of the problems with the "Pyramid of Doom" style callbacks, and a couple of different ways to deal with them. Promises aren't currently a native feature in Node. However, they have been included in the ES6 specification so it's just a matter of time.

In the meantime, there are a plethora of excellent promise libraries that you can use today:

I'm excited for the future of Node, and Javascript in general. Additional features like async/await will really improve the asynchronous programming model, as they already have for C#.