A fresh new look at callbacks

Most node.js developers know how messy a piece of code could look like with several layers of nested callbacks. The solution to callback hell, in everyone’s mind now, is Promises. But what if there is an alternative way as well? How about taking a fresh new look at managing callbacks?

Before I show the proposed solution, lets consider what makes nested callbacks look messy? Code that was supposed to look like the following when used synchronously:

action3(action2(action1()))
//or if the operation is chainable
action1().action2().action3()

looks like following when done asynchronously:

action1(function (err, res) {
action2(res, function (err, res) {
action3(res, function () { /* ... */});
});
});

Mostly what is annoying about async is the indentation right?

No, there is more. We’ll lets take a more sophisticated example that involves an ‘if’ condition (sync code):

var res = action1();
res = (res ? action2() : action3());
action4(res);
/* ... */

Now the async equivalent starts to look ugly:

action1(function (err, res) {
var action4Handler = function () { /* ... */};
if (res) {
action2(function (err, res) {
action4(res, action4Handler);
});
} else {
action3(function (err, res) {
action4(res, action4Handler);
});
}});
});

You see how we had to define an action4Handler much before using action4()? Generally in sync code we read the code top-to-bottom, but now when we switched to async, the code starts spreading all over the place. It is breaking the “linear flow” of the code and that is what’s adding cognitive load — what’s impeding our comprehension of the code — and what, I believe, is the root of our uneasiness with callbacks.


Metanoia

Promises are just one way of managing asynchronous code. There is another way as well. Introducing flow.js:

flow(function (p) { //queue of "tasks"/functions. task 1
action1(p.next);
}, function (p, err, res) { //task 2
(res ? action2 : action3)(p.next);
}, function (p, err, res) { //task 3
action4(res, p.next);
}, function (p, err, res) {
/* ... */
})();

How does this work? I haven’t further changed the signature of the action functions. They still accept callbacks that expect (err, result) as arguments like the last snippet. However, instead of passing an anonymous function as callback, I pass p.next. When p.next gets invoked, flow.js forwards the err and result to the next task/function in the queue.

Does this seem easier than promises? Or the same? Share your thoughts.

PS: The point of this article isn’t that “you should use flow.js”, rather it is that “there can be another way of doing things”.

Follow me on twitter — @munawwarfiroz