Writing solid asynchronous code using Promises

Writing solid asynchronous code using Promises

There are two approaches to handling asynchronous control flow in JavaScript at the moment: Callbacks and Promises. Node.js core APIs, and thus most things written for Node, are callback based. I’m not claiming that callbacks should never used, but I believe Promises simply are a better solution to the same problem. Whereas callbacks are “the simplest solution that would work”, Promises is a thought-out solid solution that, we will see, don’t break your code!

Callbacks

fs.readFile('data.json', function(err, data) {
  if (err) // handle it
  try {
    var json = JSON.parse(data);
    render(json);
  }
  catch (e) { // handle it }}
});

Node.js core APIs uses callbacks to allow your to write code that executes after an I/O operation is either finished or erred. The case for callbacks is that adhering to Node.js APIs brings familiarity for other developers. The function(err, data) pattern will not raise eyebrows for other JavaScript developers. The case against callbacks is that they break the exception system, it brings Callback Hell, it requires you to write your functions wholly differently that you’d do if they weren’t async. Who takes an err as the firstargument? And they even break language (any language) standards such as Exceptions!

The Callback Hell argument is weak as there is nothing in the pattern forcing you to write ten levels of callbacks nested as anonymous functions inline, but the pattern does make it easy for you as a developer to do so.

Promises

Promises is a coming ECMAScript 6 specification that’s extracted from open source libraries providing this functional approach to solving control flow for async code. The concept behind promises is that when returning a Promise you are returning an object that promises to either successfully resolve with a value or to reject with a reason.

Thenables

Promises are sometimes called thenables, referring to one of the most important parts of the specification stating that every Promise must have a method then that takes one or two functions as arguments, a resolvedHandler and a rejectedHandler. Every such handler must then return a new Promise — often handled by the library for you as well — thus achieving chaining: promiseMe().then().then().then(resolved).

Some of the important benefits promises brings to the callback-table are that they

… don’t force me to rewrite all of my code

The nice thing about promises is that they don’t interfere with a functions arguments but rather strictly adheres to arguments as input and return values as output. Callbacks are notoriously bad because they conflate inputs and outputs for a function. Promises are also interchangeable so instead of passing a callback 5 levels deeper you can just return the promise in every function — even changing it on the way — before you grab it from the entry point.

var nestedPromises = function() {
    return new Promise(function(resolve, reject) {
        new Promise(function(resolve2, reject2) {
            resolve2("Hello world");  
        }).then(resolve, reject)
    })
}

nestedPromises().then(logIt);

It is true that some code at some point has to change to create the promises, but most changes can be kept at wrapping function calls.

… will compose

Promises are composable so you can achieve g(f(x)), an impossible feat using callbacks. The syntax changes so this isn’t perfect either, but supporting square(square(2)) would require more cruft in the square function.

var square = function(num) {
    return Promise.resolve(num * num); 
}

square(2).then(square).then(logIt);

Lets compare this with how we would solve this out of the box with callbacks:

var squareCb = function(num, cb) {
  cb(null, num * num);
};

squareCb(2, function(err, num) {
  squareCb(num, function(err, result) {
    logIt(result);
  });
});

Lets take the Promise composition example and adapt it to use an existing sync square method to prove that we can wrap existing code into promises/thenables:

var square = function(num) {
    return num * num;
}

Promise.resolve(square(2)).then(square).then(logIt);

… exceptions!

Promises correctly bubbles exception upwards, aligning Promise based code with its synchronous counterpart. Look at this callback implementation, it’s obviously convoluted and brittle:

getUser(userId, function(err, user) {
  if (err) handleError(err);
  else {
    getUserFriends(user, function(err, friends) {
      if (err) handleError(err);
      else {
        getUserProfile(friends[0], function(err, json) {
          if (err) handleError(err);
          else {
            try { 
              var bestFriend = JSON.parse(json);
              refreshUi(bestFriend);
            }
            catch (Exception e) {
              handleError(e.message);
            }
          }
        });
      }
    });
  }
});

Lets write the same code using Promises:

getUser(userId)
  .then(function(user) {
    return getUserFriends(user);
  })
  .then(function(friends) {
    return getUserProfile(friends[0]);
  })
  .then(JSON.parse)
  .then(frefreshUi, handleError);

Promises uses plain normal exceptions for its error handling, meaning we automatically can work with built-ins like JSON.parse in this example.

Pitfalls of Promises

Wrapping node core API

Unfortunately we need to fight Node.js when going pure Promises. It introduces some complexity but promises libraries help us out with wrapper methods:

var Promise = require('bluebird');
// Entire library
var fs = Promise.promisifyAll(require('fs'));
fs.readFile('foo.json').then(JSON.parse).then(console.log.bind(console));

// Method
var request = Promise.promisify(require('request'));
request('http//foo.com/foo.json').then(JSON.parse).then(console.log.bind(console));

I’m in a romantic relationship with async.js

You’re in luck! Most things you thought async kicked ass at, Promises pretty much one-ups! Lets take something a little “complex” — the auto example for async.js and the same written using Promises:

async.auto({
    get_data: function(callback){
        console.log('in get_data');
        // async code to get some data
        callback(null, 'data', 'converted to array');
    },
    make_folder: function(callback){
        console.log('in make_folder');
        // async code to create a directory to store a file in
        // this is run at the same time as getting the data
        callback(null, 'folder');
    },
    write_file: ['get_data', 'make_folder', function(callback, results){
        console.log('in write_file', JSON.stringify(results));
        // once there is some data and the directory exists,
        // write the data to a file in the directory
        callback(null, 'filename');
    }],
    email_link: ['write_file', function(callback, results){
        console.log('in email_link', JSON.stringify(results));
        // once the file is written let's email a link to it...
        // results.write_file contains the filename returned by write_file.
        callback(null, {'file':results.write_file, 'email':'[email protected]'});
    }]
}, function(err, results) {
    console.log('err = ', err);
    console.log('results = ', results);
});

Promise
  .all([get_data, make_folder])
  .spread(function write_file(data, folder) { return 'filename'; })
  .then(function email_link(filename) {
    return sendEmail({file: filename});
  })
  .catch(function(err) { console.log(err); });

Having said so, async.js is still an awesome library with a ton of great control flow helpers.

Promises are slow!

This used to be a valid argument but with the Bluebird Promise library the arguments moot. Gorgi Kosev shared an analysis of async patterns where Bluebird is almost as fast as plain callbacks, and over twice as fast as async.js. The same is the case for memory usage.

Get me some Promises

There’s a lot of Promises implementations popping up, but I will recommend three concrete ones:

  • Q.js — One of the older and more mature libraries. Heavily used and feature rich
  • RSVP.js — Written for Ember by Yehuda Katz
  • Bluebird.js — My favourite library. Unmatched performance

I would also recommend you to learn more from people smarter than me who has written about Promises: