Callback hell is a design choice.


Repeated comments and blog posts about the “callback hell” problem in JavaScript on sites such as HN and Reddit have really baffled this JavaScript programmer.

Callback hell is where you call an asynchronous function which provides the result of its computation via a callback function which is passed in as an argument to the asynchronous function.

Then inside the callback function you call another asynchronous function which requires a different callback function and so on… this pattern can be repeated to a risible depth.

It’s been described as “where code marches to the right faster than it moves forward“.

//  Asynchronous function
//    |
range.on("preheat", function() { //Callback function
//    Asynchronous function
//      |
    pot.on("boil", function() { //Callback function
//    Asynchronous function
//           |
        rice.on("cooked", function() { //Callback function
            dinner.serve(rice);
        });
    });
});

The end result is code that becomes progressively more difficult to read, understand and change.

The part that baffles me is that I’ve been writing JavaScript for several years and I’ve never once created such a nested callback structure. After having written (an admittedly simple) Node.js module which reads directories, files and operates on the files I’ve come to the conclusion that callback hell is a design choice and not an inherent flaw in the concept of asynchronous function + callback.

It’s all down to the JavaScript code style used.

Writing JavaScript in an OO manner makes callback hell a non issue. While writing JavaScript in a FB (Function Blobs) manner can cause scaling issues, one of which is callback hell.

Unfortunately most of the code samples on the internet appear to be written in the FB manner and that may mean a lot of people learn a programming style that’s quick and easy to get going with but has difficulty scaling.

A quick example of the OO style I’m speaking about is this Node.js code that reads file system directories and has the result of the read returned asynchronously.

For those not familiar with the Node.js libraries

var fs = require('fs');

returns the File System module. This module provides an asynchronous method readdir which takes two arguments the first being the directory to read and the second being our callback function which is passed in the files found in the directory or an error.

var fs = require('fs');
var Class = require('./Class');
var Library = require('./Library');

function DirectoryScannerConstructor() {
    //Constructor.
}

var DirectoryScanner = DirectoryScannerConstructor.prototype;

DirectoryScanner.scanSource = function(srcRoot) {
    // Asynchronous Function    Callback function
    //    |                         |
    fs.readdir(srcRoot, this.onSourceDirRead.bind(this));
};

DirectoryScanner.scanLibraries = function(librariesRoot) {
    // Asynchronous Function    Callback function
    //    |                             |
    fs.readdir(librariesRoot, this.onLibrariesDirRead.bind(this));
};

DirectoryScanner.onSourceDirRead = function(error, files) {
    //The source directory has been read, process the files within.
    //Create objects that take in the new file locations
    //and read their data in.
    for(var file of files)
    {
        new Class(file);
    }
};

DirectoryScanner.onLibrariesDirRead = function(error, files) {
    //The library directory has been read, process the files within.
    //Create objects that take in the new file locations and
    //read their data in.
    for(var file of files)
    {
        new Library(file);
    }
};

No nested callbacks and the code that reads directories is encapsulated in a different module/class to the one that reads source files (Class class) and Libraries (Library class). Those classes can themselves read the contents of the file locations they are passed in.

The code above passes in a method, bound to the object instance using bind() as the callback function. This allows the code that processes the asynchronous function’s return value to be encapsulated in a method and removes the need for callback indenting.

So take an OO approach to your JavaScript and you should soon see an end to callback hell.

P.S.

The other advantage of using this approach is you can create Partial Functions. These allow you to pass extra arguments to your method callback when the asynchronous function calls you back with the results of its computation.

var extraArgument = 10;

fs.readdir(srcRoot, this.sourceDirRead.bind(this, extraArgument));

DirectoryScanner.sourceDirRead = function(extraArg, error, files) {
//When the function is called back with error/files we will also be
//provided with 10 as the value of the extraArg parameter.
}

Update: A comment on HN points out that they prefer promises, I like promises and believe they will be even more powerful once Generators are broadly supported (see Task.js) but when dealing with code that doesn’t return promises I think this is a clean and simple approach.

Related Posts with Thumbnails

2 Comments

  • The post misses the point of inline callbacks in that they are closures and therefore have access to the ancestor’s scope.

    I’ve not experimented with .bind() since it’s not fully compatible with old browsers (which I still have to support), but I’m thinking if I have to push the context onto the heap… well I don’t know if I can access earlier variables in the closure, could someone please educate me here?

  • Adam Iley says:

    Closures do have access to the surrounding scope, but (and often this is more useful) methods have access to the object scope. On top of that, anything in the surrounding scope that you need access to you can simply bind to an argument that you pass in to the object or add to the object scope.

    I think the authors whole point here is that having the relevant state encapsulated in an object is much nicer than having pieces of state spread through multiple layers of closures.

    I think your question is about whether or not you can access in a nested function state from distance nesting ancestors of your function. If that is your question, the answer is yes, although as the post says, this is not a sensible way to manage state.

Leave a Comment

*
To prove you're a person (not a spam script), type the security word shown in the picture. Click on the picture to hear an audio file of the word.
Anti-spam image