Avoid callbacks in Node.js with Bluebird promises - migration-bluebird-nodejs

In this article: Common callback usage patterns would be replaced with idiomatic Bluebird

Required knowledge: Basic Node.js experience, basic experience with Promises

Basic usage – a sequence of tasks:

<CallbackSequence.js>

const fs = require('fs');
fs.readFile('someFile.txt', 'utf8', function(err, fileContent) {
   if(err) {
     throw err; // Actually ends process
   }
   fs.readFile(fileContent, function(err, image) {
     if(err) {
       throw err;
     }
     doStuff(image);
   });
});

VS.

<PromiseSequence.js>

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
fs.readFileAsync('someFile.txt', 'utf8')
  .then(fileContent=>fs.readFileAsync(fileContent))
  .then(doStuff);
  .catch(err=>{
    // Handle all errors in one place
  });

As we can see, the maximal nesting level is reduced. Error handling point is well-defined, so we can’t make a mistake while handling errors – like parsing error to a bad higher-level callback.

Branching

Promise based solution isn’t silver-bullet. To be honest I had to include common use case of branching when Promise based solution is only barely better than callback-based flow.

<BranchingCallback.js>

const basicArithmeticsAsService = require('fictional-service');
basicArithmeticsAsService.add(2,2, (err, response) => {
  if (err) {
    return someCallback(new TypeError('Network issues': + err.message));
  }
  if (response.status === 200) {
    return basicArithmeticsAsService.multiply(response.body, 4, (err, response) => {
        if (err) {
          return someCallback(new TypeError('Network issues': + err.message));
        } else if (response.status === 200) {
          return someCallback(null, response.body)
        } else {
          someCallback(null, NaN);
        }
    })
  } else {
    someCallback(null, NaN);
  }
});

VS.

<BranchingPromise.js>

const basicArithmeticsAsService = require('fictional-service');
basicArithmeticsAsService.addAsync(2,2)
  .then((response) => {
    if (response.status === 200) {
      return basicArithmeticsAsService.multiplyAsync(response.body, 4)
        .then(response=>{
          if (response.status === 200) {
            return response.body;
          } else {
            return NaN;
          }
        })
    } else {
      return NaN;
    }
  })
  .catch(err=>{
    throw new TypeError('Network issues': + err.message);
  });

Async.js

Async.js became the industry standard for handling callbacks before Promises went viral. Today, most of the async.js utilities can be replaced with idiomatic usage of advanced Promise implementations, with all of it richness – promisification, convenient adapters for callbacks, long stack traces.

Let’s analyze basic asynchronous flows:

<AsyncFilter.js>

const fs = require('fs');
const async = require('async');
async.filter(['file1','file2','file3'], function(filePath, callback) {
  fs.access(filePath, function(err) {
    callback(null, !err)
  });
}, function(err, results){
    // results now equals an array of the existing files
});

VS.

<BluebirdFilter.js>

const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs'));
Promise.filter(['file1','file2','file3'] , path=>fs.accessAsync(path))
.then(results=>{

});

Bluebird exposes common array operations like map, filter, each. These methods take 3 arguments – array (or Promise to resolve to an array), function applied to each element and concurrency – maximal number of functions running at once, in a manner similar to async.fnLimit family.

Some async.js methods for collections can’t be emulated using basic Promise-based code – methods like ‘sortBy’, ‘transform’ tend to be a pain in the ass. We’ll discuss how to marry async.js and Bluebird later.

Retry

It’s often convenient to retry failed task. It’s quite easy to write in a synchronous manner, but writing it in an asynchronous way can be mind-boggling, especially when concurrency or parallelism comes in the hand.

<RetryCallback.js>

const async = require('async');

retry(5, done=>getResource(url, done), (err, result)=>{
  if (err) throw err;
})

VS.

<RetryPromise.js>

const async = require('async');
function retry(n=5, task) {
  return Promise.resolve().then(task).catch(err=>{
    if (n > 1) return retry(n-1, task);
    throw err;
  });
}

retry(()=>getResourceAsync(url));

Bluebird offers a better solution than callback-based approach, because of it’s superior resource management. Consider this code:

<RetryResourceCallback.js>

function retry(fn, maxRetries, complete) {
   fn((err, res)=>{
     if (err) {
       if (!maxRetries) {
         complete(err);
       } else {
         retry(fn, maxRetries-1, complete);
       }
     } else {
       complete(err, res);
     }
   })
}

retry(complete=>{
    return async.parallel([
       aquireResource,
       aquireOtherResource
    ], complete);
}, 5, console.log)

Can you see what is wrong with this code? It runs to the successful completion of all tasks, or first rejection. If acquiring resource is time-consuming, we’ll introduce to our code race conditions – code execution (in our case – retry) will continue regardless if the non-faulty task released its resources.

Bluebird offers powerful mechanisms to prevent this issue. Disposers allow us to automatically clean resources, similar to RAII pattern, but for better comparison with async we will assume that our functions task1 and task2 clean their resources on completed execution.

<RetryResourcePromise.js>

function settleAll(promises) {
  return Promise.all(promises.map(promise => Promise.resolve(promise).reflect()))
    .then(function(results) {
      const err = results.find(e=>e.isRejected());
      if (err) {
        throw err.reason;
      } else {
        return results.map(e=>e.value);
      }
  });
}

function retry(task, maxRetries) {
  return task().catch(e=>{
    if (!maxRetries) return retry(task, maxRetries-1);
    throw e;
  });
}

retry(()=>settleAll([task1(), task2()]), 5).then(console.log, console.error);

Want to know more? Worth to read:

Let's chat!

Avoid callbacks in Node.js with Bluebird promises - marcel-100px Hi, I’m Marcin, COO of Applandeo

Are you looking for a tech partner? Searching for a new job? Or do you simply have any feedback that you'd like to share with our team? Whatever brings you to us, we'll do our best to help you. Don't hesitate and drop us a message!

Drop a message
Avoid callbacks in Node.js with Bluebird promises - Start-a-project