Moshe's Blog

Tips and tricks learnt along the way.

Future Proof Your Code With Promises and Promise.all

| Comments

When starting out with async code there are a few option to manage the callback hell. The one gaining the most traction is promises.

I starting writing a promise-like library when I first heard of promises: WTTT (When This Then That)

I was playing around with socket.io and had to do some async code as follows:

socket.on('join', function(id, name, callback) {
  socket.join(id);
  socket.name = name;
  var people = _.map(io.sockets.clients(room), function(socket) {
    return {
      id: socket.id,
      name: socket.name,
    };
  });
  callback(people);
});

Looking at the docs show that I should be using an async version as follows:

socket.on('join', function(id, name, callback) {
  socket.join(id) // this is still a sync function
  socket.set('name', name, function() {
    var people = ???;
    callback(people);
  });
});

As you can see there’s some magic that we need to do. Enter promises.

The basic usage is as follows

var promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve("It's now two seconds later");
  }, 2000);
});
promise.then(function(value) {
  // value === "It's now two seconds later";
});

The library that I’m using is bluebird, they provide a Promise.promisifyAll method which takes an object and converts all it’s methods that have node style callbacks (function callback(err, result) {...}) as a last argument to promises

Promises by itself is not really that usefull, but the real power comes when you need to do many things at once, that’s what Promise.all is for

var resolveTo = function(thing) {
  return new Promise(function(resolve) {
    resolve(thing);
  });
};
var promises = [resolveTo('Apple'), resolveTo('Orange')]
Promise.all(promises).then(function(results) {
  // results === ['Apple', 'Orange'];
});

Promise.all also can take a non-thenable value and use it as is for example

var promises = [resolveTo('Apple'), resolveTo('Orange'), 42]
Promise.all(promises).then(function(results) {
  // results === ['Apple', 'Orange', 42];
});

Armed with this we can now write the above socket code as follows

socket.on('join', function(id, name, callback) {
  socket.join(id) // this is still a sync function
  socket.setAsync('name').then(function() {
    getPeopleAsync(id).then(function(people) {
      callback(people);
    });
  });
});
function getPeopleAsync(roomId) {
  return new Promise(function(resolve, reject) {
    var people = io.sockets.clients(room);
    var promises = [];
    people.forEach(function(socket) {
      promises.push(
        new Promise(function(resolveInner) {
          Promise.all([socket.id, socket.getAsync('name')]).then(function(results) {
            resolveInner({id: results[0], name: results[1]});
          });
        });
      );
    });
    Promise.all(promises).then(function(people) {
      resolve(people);
    });
  });
}

Which getPeopleAsync can be refactored to:

function getPeopleAsync(roomId) {
  return new Promise(function(resolve, reject) {
    var people = io.sockets.clients(room);
    var promises = [];
    people.forEach(function(socket) {
      promises.push(
        new Promise(function(resolveInner) {
          Promise.all([socket.id, socket.getAsync('name')]).then(function(results) {
            resolveInner({id: results[0], name: results[1]});
          })
        })
      );
    });
    Promise.all(promises).then(resolve);
  });
}

Which getPeopleAsync can be refactored to:

function getPeopleAsync(roomId) {
  var people = io.sockets.clients(room);
  var promises = [];
  people.forEach(function(socket) {
    promises.push(
      new Promise(function(resolve) {
        Promise.all([socket.id, socket.getAsync('name')]).then(function(results) {
          resolve({id: results[0], name: results[1]});
        })
      })
    );
  });
  return Promise.all(promises)
}

Now this lets us use Promise, notably Promise.all without having to worry about code ever changing from sync to async, consider:

Promise.all(socket.getAsync('name'), socket.getAsync('joined'), serverId, personRecord).then(function(results) {
  // do things with results
});

and

Promise.all(socket.getAsync('name'), socket.getAsync('joined'), serverId, db.getPersonAsync(personId)).then(function(results) {
  // do things with results
  // absolutely no changes needed here
});

Happy coding

Comments