By Oleksii Rudenko October 21, 2015 4:29 PM
Best Practices for Using Promises in JS

Promises are nice abstractions for working with asynchronous operations. A promise acts as a proxy that provides you with the result of deferred/asynchronous computations, be it some data or an error. Promises are similar to normal callbacks but it’s more easy to work with them because they are objects. Compare the following snippets:

Without Promises:

doAsync(function(err, data) { //cb
  if (err) {
    // error
  } else {
    // success
  }
});

with Promises:

var promise = doAsync();
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

So these two are quite similar but promises allow you to define a callback later, after you start an asynchronous operation. Also there are two callbacks - one for errors and one for the happy path. And even if the promise is resolved before you assign a callback, the callback will be called anyway and you will get the result of the promise.

Promises are more and more prevalent nowadays so I will skip directly to what I consider to be the best practices (in 2015) of using them.

1.) Build you interfaces using Promises. Instead of asking to provide a callback to your function, return a Promise.

function doAsync() {
  return new Promise(function(resolve, reject) {
    // some code that fills in err if there is an error
    if (err) {
      reject();
    } else {
      resolve();
    }
  });
}

This gives you several advantages:

  • Your interfaces are not polluted by cb parameter.
  • It’s easier for consumers of your API to work with Promises.
  • Promises can be yielded in a generator function.
  • Promises can be combined with async functions (ES7)

2.) In nodejs, always use bluebird(or similar) as Promise implementation, even if your node version provides a native one. Just do the following:

  var Promise = require('bluebird');

Advantages:

3.) Don’t define rejection handlers as the second argument for then calls, always use an extra catch. This means that instead of

var promise = doAsync();
promise
  .then(function(data) { //cb
    //done
  }, function(err) { // second argument is an error handler
    // success
  });

use catch

var promise = doAsync();
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

Here is the explanation.

4.) Flatten Promise chains

If you just start with Promises, it’s some natural to start writing the code like this:

  return service
    .do()
    .then(function(result) {
      return service
        .do2(result.fieldA)
        .then(function(result2) {
          return service
            .do3(result2.fieldB)
            .then(function(result3) {
              return result3.fieldC;
            });
        });
    })

Instead, it’s better to rewrite it like this:

function do1() {
  return service
    .do()
    // some transformations
    .then(result => result.fieldA);
}

function do2(resultOfDo1) {
  return service
    .do2(resultOfDo1)
    // some transformations
    .then(result => result.fieldB);
}

function do3(resultOfDo2) {
  return service
    .do3(resultOfDo2)
    // some transformations
    .then(result => result.fieldC);
}

return do1()
      .then(do2)
      .then(do3);

5.) Use Promise.all, Promise.spread and other methods to control the flow

If you want to run some operations in parallel, use Promise.all:


var parallel = function(do1Param, do2Param) {
  return Promise.all([
    do1(do1Param),
    do2(do2Param)
  ])
}

parallel(1, 2)
  .then(function() {
    //both promises are resolved
  });

If you need to get access to results of promises running in parallel, use Promise.spread:

parallel(1, 2)
  .spread(function(do1Result, do2Result) {
    // results are available here
  });

6.) To limit concurrency use Promise.map or similar

var data = [ // some data representing tasks each taking 2s
  2000,
  2000,
  2000,
  2000,
  2000,
  2000,
];
var i = 1; // promise counter
var task = function(timeout) {
  return new Promise(function (resolve) {
    console.log('Running promise ' + i);
    i++;
    setTimeout(resolve, timeout);
  });
};

// iterate over data and run at most 3 tasks in parallel
Promise.map(data, task, { concurrency: 3 }).then(function() {
    console.log("done");
});

Anything else? What would you suggest as a best practice?

Thanks for reading.