ControlAltDel ControlAltDel - 26 days ago 10
Node.js Question

node.js modules: Async vs Fibers.promise vs Q_oper8

Just wondering if anyone could give me a comparison of trade-offs between these modules for handling async events. Specifically, I'm interested in knowing about reasons to use Async instead of Fibers.promise, which I am using quite extensively at least in my test code right now. In particular, one of the major pluses I see in Fibers.promise is that I can keep the stack chain front bifurcating, making it possible to use

try { } catch { } finally
, and also allowing me to ensure that after a request has been handled that the response is ended.

Is anyone using Q_oper8? I found this on another page and was just wondering if that's already dead or if its something I should check out.

Answer

I've never heard of Q_oper8, so I can't comment on it, but I'll come at this from the other direction. I heard about async first and Fiber (and its helper libraries) second, and I don't like the latter, actually.

The Downsides of Fiber

Unfamiliarity for other Javascript developers

Fiber introduces the concept of co-routines to Javascript via a compiled Fiber native method that takes over the interpretation of the Javascript code passed to it, intercepting calls to yield to jump back to the waiting co-routine.

This may not matter to you, but if you need to work on a team, you'll have to teach the concept to your members (or hope they have experience with the concept from other languages, like Go).

No Windows Support

So, in order to use Fiber or any of the libraries written on top of it, you'll have to natively compile it for your platform first. I don't use Windows, but note that Fiber is not supported on Windows, so that restricts the utility of your own library off-the-bat. Which means you won't be finding general-purpose Node.js libraries written in Fiber at all (and you probably wouldn't have, anyways, since it adds a costly compilation step that you'd otherwise avoid with async).

Browser Incompatible

This means any code you write using Fiber will not be able to run in the browser, because you can't mix native code with the browser (nor would I as a browser user want you to), even if everything you write is "Javascript" (it's syntatically Javascript, but semantically not).

More Difficult Debugging

While the "callback hell" may be less visually pleasing, Continuation-Passing Style does have one very good thing going for it over Co-Routines -- you know exactly where a problem has occurred from the call stack and can trace backwards. Co-Routines enter the function at more than one point in the program, and can exit from three kinds of calls: return, throw and yield(), where the latter is also a return point.

With co-routines, you have cross-execution between two or more functions running "simultaneously", and you may have more than one set of co-routines running at the same time on the event loop. With traditional callbacks, you're guaranteed that the outer scope of the function is static during the execution of said function, so you only need to check those outer variables once if they're needed. Co-routines need these checks to be run after every yield() (since it's usage with the originating co-routine would be translated into a callback chain in real Javascript).

Basically, I think the co-routine concept is made more difficult to work with because it has to exist inside of the Javascript event loop, rather than being a method to implement one.

What makes Async "better"?

Worse is Better

It's sort of the "worse-is-better" idea, actually. Rather than extend the Javascript language to try and get rid of its warts (and create new ones, in my opinion), Async is a pure-Javascript solution to cover them up, like makeup.

Control flow explicit

The Async functions describe different types of logic flow that needs to cross the event loop barrier, and the library covers up the implementation details of the callback code needed to implement that logic, and you just provide it functions it should run in roughly the linear order they will execute across the event loop.

If you're willing to drop the first indentation level around the async methods' arguments, you have no extra indentation versus Co-Routines and only a minor number of extra lines of function(callback) { declarations, like this:

var async = require('async');
var someArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];
async.forEach(someArray,
function(number, callback) {
    //Do something with the number
    callback();
}, function(err) {
    //Done doing stuff, or one of the calls to the previous function returned an error I need to deal with
});

In this case, you know that all of the variables your code is using could only have been changed before your code is run if they weren't changed by your code, so you can debug easier, and there is only one "return" mechanism: callback(). You either callback with nothing on success or pass the callback an error when something's gone wrong.

Code reuse not difficult

The above example makes code reuse difficult but it doesn't have to be. You can always pass in named functions as the parameters:

var async = require('async');

// Javascript doesn't care about declaration order within a scope,
// so order the declarations in a way that's most readable to you

async.forEach(someArray, frazzleNumber, doneFrazzling);

var someArray = [1, 2, 3, 4, 5, 6, 7, 8, 9];

function frazzleNumber(number, callback) {
    // Do something to number
    callback();
}

function doneFrazzling(err) {
    // Do something or handle error
}

Functional, not imperative

The async module discourages the use of imperative-style flow control and encourages (requires, for the parts that cross the event loop) the use of functions for flow control.

The advantage of the functional style is that you can easily re-use the body of your loop or your conditional, and that you can create new control flow "verbs" that better match the flow of your code (demonstrated by the very existence of the async library), like the async.auto control flow method that implements dependency graph resolution for function call order. (You specify a series of named functions and list the other functions, if any, that it depends on to execute, and auto runs first the "independent" functions then the next function that can run based on when its dependent functions have finished running.)

Rather than writing your code to fit the imperative style dictated by your language, you write your code as the logic of the problem dictates, and implement the "glue" control flow to get it to happen.

In Summary

Fiber, by its very nature of extending the Javascript language, cannot develop a large ecosystem within Node.js, especially when Async gets 80% of the way on the looks department, and has none of the other downsides of co-routines in Javascript.

Comments