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
yield
ed 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:
- It acts as a thin compatibility layer and abstracts away differences in implementations.
- It provides a better performance. Though I guess the native implementations will catch up soon.
- It provides additional useful features.
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.