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.

Original source