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 – 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, maximal nesting level is reduced. Error handling point is well-defined, so we can’t make a mistake while handling errors – like passing error to 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 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 resolving to array), function applied to each element and concurrency – maximal number of functions running at once, in 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 pain in 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 synchronous manner, but writing it in 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 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 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 non-faulty task released it’s resources.

Bluebird offers powerful mechanisms to prevent this issue. Disposers allow us to automatically clean resources, similar to RAII pattern, but for better comparision 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: