task.js generators/promises vs. async callbacks
asynchronous, javascript, node.js
Solution
The `async` library encapsulates a couple of very common asynchronous patterns, including making arbitrary async calls in parallel and iterating over a list asynchronously. It's designed to work with "nodeback" `(err, res)` APIs, which makes it useful for a lot of Node.js applications. `async` is however a specific solution, and it only simplifies the async patterns included in the library.
Promises, by contrast, are in my opinion a much more general solution to the problem of asynchronous code. Not only do they provide the obvious benefits at first glance of error-bubbling and of flattening callback pyramids, problems that would otherwise require the sorts of complex patterns `async` encapsulates can be solved much more simply.
I'll demonstrate this with a quick tour through some of `async`'s available patterns. For instance, the `async.waterfall` function is used something like this:
async.waterfall([
function (cb) {
asyncCall('argument', cb);
},
function(resultOfFirstCall, cb) {
anotherCall(resultOfFirstCall, 'someOtherArgument' cb);
},
], function(err, res) {
if (err) handle(err);
useFinalResult(res);
});
There's no equivalent to `async.waterfall` in most promise libraries (or at least there isn't one in Q), because it's so simple to implement it from scratch using `Array.reduce`, like so (example based on Q but pretty much the same on other promise libraries):
[
function() {
return asyncCall('argument');
},
function(resultOfFirstCall) {
return anotherCall(resultOfFirstCall, 'someOtherArgument');
}
].reduce(Q.when, Q())
.then(useFinalResult, handle);
The other big functions in `async` include `async.parallel`, which Q includes as `Q.all`:
// async
async.parallel([
asyncFunc,
asyncFunc2
], function(err, res) {
if (err) handle(err);
useFinalResult(res);
// res[0] === asyncFuncResult
// res[1] === asyncFunc2Result
});
// Q
Q.all([
asyncFunc(),
asyncFunc2()
]).then(useFinalResult, handle);
And `async.map`. You actually don't need `async.map` when you're using promises, because the normal `Array.map` is sufficient:
// async
async.map(['file', 'file2', 'file3'], fs.stat, function(err, res) {
if (err) handle(err);
useFinalResult(res);
});
// Q
Q.all(['file', 'file2', 'file3']
.map(Q.nfbind(fs.stat)))
.then(useFinalResult, handle);
The rest of `async` is similarly easy to implement concisely, using relatively simple pieces of your promise library. (Note that that last example used a function `Q.nfbind`: `nfbind` and the other `nf*` functions Q provides are basically all you need to use promises with nodeback APIs, so there isn't even a particularly big impedance trying to use promises with libraries that expect nodebacks.)
In the end, whether you use promises or nodebacks is up to you, but I think promises are a much more flexible, capable, and generally concise way to implement most all asynchronous operations.
Callbacks are imperative, promises are functional is worth a read for more information in this general vein.
Problem
I want to know which of the two methods are better when dealing with asynchronous code in JavaScript. I want to understand which method leads to cleaner code. I'm used with promises and they seem more flexible than the async approach (https://github.com/caolan/async). I also know about the Task.js library (http://taskjs.org/), but this relies on the yield keyword which is part of Ecmascript Harmony.