One of the common critiques of node.js is that its (events-based) asynchronous model requires a lot of nested callback functions and generally uglier flow control than a synchronous model would allow. An example:

fs.readFile(path.join(base, 'config.json'), function(err, data){
    if (data) {
        data = JSON.parse(data);
        data.forEach(function(path){
            fs.readFile(path, function(err, data){
                if (err) {
                    log(err)
                } else {
                    data = JSON.parse(data);
                    if (data.url) {
                        http.get(url.parse(data.url), function(){
                            // ...absurdum ad infinitum
                        });
                    }
                }
            })
        })
    }
});

This isn’t a trait unique to node.js though. Much of JavaScript, since it is a language heavily bound to the client-side and therefore DOM and user-initiated events, is very liable to “Rampant Callback Syndrome”.

Rampant Callback Syndrome

Rampant Callback Syndrome is a serious issue in the JavaScript arena and if left unchecked will continue to spread like a disease, poisoning our dreams with its ominous creep.

In all seriousness, it is rather annoying, and is definitely something that can be avoided with nothing more than a light dose of decent application design and thought.

One pattern that I’ve become fairly attached to is the simple premise of providing an API to begin with, and letting it appear to be a (mostly) synchronous interface, but behind the scenes throw around events to make it work properly.

Let’s say, for example, that we want to expose a Model API, for interfacing with our database.

When the model is instantiated we go about opening the connection to the database:

function Model() {
 
    // (not shown) ... configuration stuff ...
 
    this.server = new mongodb.Server(
        this.config.address,
        this.config.port,
        {}
    );
 
    // (not shown) ... error checking ...
 
    this.db = new mongodb.Db(this.config.dbName, this.server, {});
 
    this.db.open(function(error, client){
 
        if (error) {
            this.error('DB: ' + this.config.db + ' error: ', error);
            return;
        }
 
        this.collection = new mongodb.Collection(client, this.config.dbCollection);
 
        this._isOpen = true;
        this.emit('open');
 
    }.bind(this));
 
    // (not shown) ... other stuff ...
 
}

The problem is that our new Model call will return before the connection to the database is open. Fortunately, as you can see above, in the callback to the db.open method we are setting a flag, _isOpen, to true, and we’re emitting the open event. This Model happens to implement the events.EventEmitter interface which means we can fire and trigger on our object.

So, let’s look at our calling-code:

var myModel = new Model({ /* config */ });
 
myModel.find({ username: 'James' }, function(data) {
    // I can live with a single nested function. 
});

This is great! The code above shows none of the callback nonsense we saw earlier[1] — we’ve abstracted it away. One big problem though is the find method is going to be called before our database connection is open. But, thanks to the events interface, we can hold off until our open event fires:

Model.prototype.find = function(selector, cb){
 
    if (this._isOpen) {
 
        // If we're already open, let's go:
 
        this.collection.find(selector).toArray(cb);
 
    } else {
 
        // Otherwise, wait:
 
        var args = arguments;
        this.on('open', function(){
            this.find.apply(this, args);
            // (added stability would be remove the event listener here)
        });
 
    }
 
};

In the else block we register a new event listener, to listen for the open event, and when it fires, we’re simply going to call the very same method (find) with the very same arguments. Obviously this is only possible if we’re using callbacks (one of the arguments to find()). Obliterating all callbacks is not we’re looking for; we’re simply looking to minimise.

Abstraction is the key

We can cure 90% of Rampant Callback Syndrome by simply abstracting our code.

The great thing about the solution shown in this post is that you’re able to provide the promised API immediately following instantiation — you’re not forcing the higher level of abstraction to fuss with what is a lower concern.

[1]: Yes, I know they’re doing completely different things. My point still stands.

Thanks for reading! Please share your thoughts with me on Twitter. Have a great day!