Escaping Callback Hell

JavaScript’s nested callback style is often derided for being unmaintainable due to callback hell. I’ve been to callback hell and back only to find callback hell is just a local nick-name for a well-known circle of programmer hell, one that has little to do with JavaScript’s callbacks.

If you are not familiar with JavaScript, it’s a funny language in that it was created primarily for controlling UI elements on a page.  Normally, one assigns functions to variables (known as callbacks) to page items, such as a button.  All buttons have a callback variable known as onClick.  If you assign a function to that onClick variable, it is called when the user clicks the button:

button.onClick = function(){
  //do something
}

JS is single threaded, in order to keep the UI responsive, JS will pause a function that takes too long to finish executing and move onto the next one.  For example, if the //do something above calculates a complex equation, waiting for it to finish would freeze the functionality of the UI.  A user might click another button, but it would not execute until the long-running function finishes.  If you have functions that must be executed sequentially, you trigger them via a callback function:

function aFunc(callback){
  var aVar = //do something;
  callback(aVar);
}

function bFunc(aVar){
//do something with aVar 
}

aFunc(bFunc)

As I began to create more complex programs, I slowly shifted awy from writing sequential, Java-style code to writing async, JavaScript code with lots of callbacks.  This shift was very painful as it happened only in fits.  The problem is that we usually start by writing the control logic while stubbing out complex functions:

var a = null;
if(x){
 a = doSomething('this way');
}
b = doSomethingElse(a);
c = anotherFunction(b);

function doSomething(variable){
  return dummyValue;
}

Because the stubs just return dummy variables, everything works out fine until you start fleshing the stubs out.  Even so, most functions return soon enough that the async behavior isn’t encountered by the programmer in a predictable fashion, instead, functions that were working just start breaking.  If you are doing any sort of I/O (such as calling a database) or processing that can take a variable amount of time (such as processing the X values that the database returns) the breakage is entirely inconsistent.  On your local machine with all of your small test values, everything works perfectly.

In the worst-case scenario, these problems don’t pop up until you deploy the code.  You try to fix things by checking the environment and slowly backtrack to rewriting functions until you eventually arrive at where you started: the control logic.  You bang your head on the keyboard a few times and (since you just wasted a huge amount of time debugging code) you do as “simple” of a hack as you can to fix the flow control logic.

At this stage “simple” means two things: “sequential logic whenever possible” and “rewriting as few functions as possible.” This is partly due to the size of the code-base you fleshed out before things started breaking.  However, it’s also due to being uncomfortable with callback to begin with. This translates into one mangling the flow control logic by simply in-lining it into the callbacks, which is where “callback hell” gets its name:

var c = doSomething();

function doSomething(variable){
  if(x){
    doSomethingElse('this way', function(error, correct){
      if(error){
        console.log(error);
      } else {
        anotherFunction(correct);
      }
    })
  } else {
    doSomethingElse('that way', function(error, correct){
      if(error){
        console.log(error);
      } else {
        anotherFunction(correct);
      }
    })  
  }
}

This is, of course, an absurd example (and I will try to come up with a better example later) but it makes a point:  can you follow the logic flow of the above code in a glance?  I sure as hell can’t. It’s totally unmaintainable an it’s why I’m in the process of refactoring a bunch of code like the above.

But as I try to marshal the callbacks into place, I find that callback hell feels like a very familiar place: poorly modularized code.  By in-lining the control-flow logic, I took what was reasonably modular code splayed into giant functions.

What’s the solution?  Well, part of this is the due to the 4 AM nature of JavaScript: one of the many 4 AM decisions they made was to style the syntax after Java (because it was hip) while adopting a radically different control-flow paradigm.  It looks like Java, it kinda runs like Java, but it isn’t Java.

Promises are on their way with ES6, which should help.  It makes the reject() and accept() message passing very explicit.  Between CoffeeScript, Dart, and Typescript there is a lot of great experimentation going on.  Maybe we can refactor some of the syntax come up with a new name in time for ES7.

However, I think that this points to a very deep problem with how we teach JavaScript programming.  I’ve taken two college courses which covered introductory JS, one professor was a funny guy (but a terrible teacher) who described JavaScript as being, “like Java, but after a few cocktails.”  However, the professor (who was a great teacher) failed to even mention callbacks.  Even the JavaScript bible fails to really delve into the callback pattern, let alone detail how one structures a program for async behavior.  I think this points to a need to change how we teach JS, but that’s another blog-post for another day.

Powered by WordPress. Designed by WooThemes