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.