JavaScript - - By Camilo Reyes

Saved from Callback Hell

Devil standing over offce worker with receivers hanging everywhere

This article was peer reviewed by Mallory van Achterberg, Dan Prince and Vildan Softic. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Callback hell is real. Often developers see callbacks as pure evil, even to the point of avoiding them. JavaScriptʼs flexibility does not help at all with this. From the surface, it seems callbacks are the perfect foot gun, so it is best to replace them.

The good news is there are simple steps to get saved from callback hell. I feel eliminating callbacks in your code is like amputating a good leg. A callback function is one of the pillars of JavaScript and one of its good parts. When you replace callbacks, you are often just swapping problems.

A friend tells me callbacks are ugly warts and the reason to study better languages. Well, are callbacks that ugly?

Wielding callbacks in JavaScript has its own set of rewards. There is no reason to avoid JavaScript because callbacks can turn into ugly warts.

Letʼs dive into what sound programming has to offer with callbacks. My preference is to stick to SOLID principles and see where this takes us.

What Is Callback Hell?

I know what you may be thinking, what the hell is a callback and why should I care? In JavaScript, a callback is a function that acts as a delegate. The delegate executes at an arbitrary moment in the future. In JavaScript, the delegation happens when the receiving function calls the callback. The receiving function may do so at any arbitrary point in its execution.

In short, a callback is a function passed in as an argument to another function. There is no immediate execution since the receiving function decides when to call it. The following code sample illustrates:

function receiver(fn) {
  return fn();
}

function callback() {
  return 'foobar';
}

var callbackResponse = receiver(callback); 
// callbackResponse == 'foobar'

If you have ever written an Ajax request, then you have encountered callback functions. Asynchronous code uses this approach since there is no guarantee when the callback will execute.

The problem with callbacks stems from having async code that depends on another callback. I will illustrate the use of setTimeout to simulate async calls with callback functions.

Feel free to follow along, the repo is out on GitHub. Most code snippets will come from there so you can play along.

Behold, the pyramid of doom!

setTimeout(function (name) {
  var catList = name + ',';

  setTimeout(function (name) {
    catList += name + ',';

    setTimeout(function (name) {
      catList += name + ',';

      setTimeout(function (name) {
        catList += name + ',';

        setTimeout(function (name) {
          catList += name;

          console.log(catList);
        }, 1, 'Lion');
      }, 1, 'Snow Leopard');
    }, 1, 'Lynx');
  }, 1, 'Jaguar');
}, 1, 'Panther');

Looking at the above, setTimeout gets a callback function that executes after one millisecond. The last parameter just feeds the callback with data. This is like an Ajax call except the return name parameter would come from the server.

There is a good overview of the setTimeout function in this previous SitePoint article.

I am gathering a list of ferocious cats through asynchronous code. Each callback gives me a single cat name and I append that to the list. What I am attempting to achieve sounds reasonable. But, given the flexibility of JavaScript functions, this is a nightmare.

Anonymous Functions

You may notice the use of anonymous functions in that previous example. Anonymous functions are unnamed function expressions that get assigned to a variable or passed as an argument to other functions.

Using anonymous functions in your code is not recommended by some programming standards. It is better to name them, so function getCat(name){} instead of function (name){}. Putting names in functions adds clarity to your programs. These anonymous functions are easy to type but send you barreling down on a highway to hell. When you go down this winding road of indentations, it is best to stop and rethink.

One naive approach to breaking this mess of callbacks is to use function declarations:

setTimeout(getPanther, 1, 'Panther');

var catList = '';

function getPanther(name) {
  catList = name + ',';

  setTimeout(getJaguar, 1, 'Jaguar');
}

function getJaguar(name) {
  catList += name + ',';

  setTimeout(getLynx, 1, 'Lynx');
}

function getLynx(name) {
  catList += name + ',';

  setTimeout(getSnowLeopard, 1, 'Snow Leopard');
}

function getSnowLeopard(name) {
  catList += name + ',';

  setTimeout(getLion, 1, 'Lion');
}

function getLion(name) {
  catList += name;

  console.log(catList);
}

You will not find this snippet on the repo, but the incremental improvement is on this commit.

Each function gets its own declaration. One upside is we no longer get the gruesome pyramid. Each function gets isolated and laser focused on its own specific task. Each function now has one reason to change, so it is a step in the right direction. Note that getPanther(), for example, gets assigned to the parameter. JavaScript doesn’t care how you create callbacks. But, what are the downsides?

For a full breakdown of the differences, see this SitePoint article on Function Expressions vs Function Declarations.

A downside, though, is that each function declaration no longer gets scoped inside the callback. Instead of using callbacks as a closure, each function now gets glued to the outer scope. Hence why catList gets declared in the outer scope, as this grants the callbacks access to the list. At times, clobbering the global scope is not an ideal solution. There is also code duplication, as it appends a cat to the list and calls the next callback.

These are code smells inherited from callback hell. Sometimes, striving to enter callback freedom needs perseverance and attention to detail. It may start to feel as if the disease is better than the cure. Is there a way to code this better?

Dependency Inversion

The dependency inversion principle says we should code to abstractions, not to implementation details. At the core, take a large problem and break it into little dependencies. These dependencies become independent to where implementation details are irrelevant.

This SOLID principle states:

When following this principle, the conventional dependency relationships established from high-level, policy-setting modules to low-level, dependency modules are reversed, thus rendering high-level modules independent of the low-level module implementation details.

So what does this blob of text mean? The good news is by assigning a callback to a parameter, guess what? You are already doing this! At least in part, to get decoupled, think of callbacks as dependencies. This dependency becomes a contract. From this point forward you are doing SOLID programming.

One way to gain callback freedom is to create a contract:

fn(catList);

This defines what I plan to do with the callback. It needs to keep track of a single parameter, that is, my list of ferocious cats.

This dependency can now get fed through a parameter:

function buildFerociousCats(list, returnValue, fn) {
  setTimeout(function asyncCall(data) {
    var catList = list === '' ? data : list + ',' + data;

    fn(catList);
  }, 1, returnValue);
}

Note function expression asyncCall gets scoped to the closure buildFerociousCats. This technique is powerful when coupled with callbacks in async programming. The contract executes asynchronously and gains the data it needs, all with sound programming. The contract gains the freedom it needs as it gets decoupled from the implementation. Code that is beautiful uses JavaScriptʼs flexibility to its own advantage.

The rest of what needs to happen becomes self-evident. One can do:

buildFerociousCats('', 'Panther', getJaguar);

function getJaguar(list) {
  buildFerociousCats(list, 'Jaguar', getLynx);
}

function getLynx(list) {
  buildFerociousCats(list, 'Lynx', getSnowLeopard);
}

function getSnowLeopard(list) {
  buildFerociousCats(list, 'Snow Leopard', getLion);
}

function getLion(list) {
  buildFerociousCats(list, 'Lion', printList);
}

function printList(list) {
  console.log(list);
}

Note there is no code duplication. The callback now keeps track of its own state without global variables. A callback, for example, getLion can get chained with anything that follows the contract. That is any abstraction that takes a list of ferocious cats as a parameter. This sample code is up on GitHub.

Polymorphic Callbacks

What the heck, letʼs get a little crazy. What if I wanted to change the behavior from creating a comma separated list to a pipe delimited one? One problem I see is buildFerociousCats got glued to an implementation detail. Note the use of list + ',' + data to do this.

The simple answer is polymorphic behavior with callbacks. The principle remains: treat callbacks like a contract and make the implementation irrelevant. Once the callback elevates to an abstraction the specific details can change at will.

Polymorphism opens up new ways of code reuse in JavaScript. Think of a polymorphic callback as a way to define a strict contract, while allowing enough freedom that implementation details no longer matter. Note that we are still talking about dependency inversion. A polymorphic callback is just a fancy name that points out one way to take this idea further.

Letʼs define the contract. One can use the list and data parameters in this contract:

cat.delimiter(cat.list, data);

Then take buildFerociousCats and make a few tweaks:

function buildFerociousCats(cat, returnValue, next) {
  setTimeout(function asyncCall(data) {
    var catList = cat.delimiter(cat.list, data);

    next({ list: catList, delimiter: cat.delimiter });
  }, 1, returnValue);
}

The JavaScript object cat now encapsulates the list data and delimiter function. The next callback chains async callbacks, this was formerly called fn. Note there is freedom to group parameters at will with a JavaScript object. The cat object expects two specific keys, both list and delimiter. This JavaScript object is now part of the contract. The rest of the code remains the same.

To fire this up, one can do:

buildFerociousCats({ list: '', delimiter: commaDelimiter }, 'Panther', getJaguar);
buildFerociousCats({ list: '', delimiter: pipeDelimiter }, 'Panther', getJaguar);

The callbacks get swapped. As long as contracts get fulfilled, implementation details are irrelevant. One can change the behavior with ease. The callback, which is now a dependency, gets inverted into a high-level contract. This idea takes what we already know about callbacks and raises it to a new level. By reducing callbacks into contracts, it lifts up abstractions and decouples software modules.

What is so radical is that from independent modules naturally flow unit tests. The delimiter contract is a pure function. This means, given a number of inputs, one gets the same output every single time. This level of testability adds confidence that the solution will work. After all, modular independence grants the right to self-assess.

An effective unit test around the pipe delimiter could look something like this:

describe('A pipe delimiter', function () {
  it('adds a pipe in the list', function () {
    var list = pipeDelimiter('Cat', 'Cat');

    assert.equal(list, 'Cat|Cat');
  });
});

Iʼll let you imagine what the implementation details look like. Feel free to check out the commit on GitHub.

Conclusion

Mastering callbacks in JavaScript is understanding all the minutiae. I hope you see the subtle variations in JavaScript functions. A callback function becomes misunderstood when one lacks the fundamentals. Once JavaScript functions are clear, SOLID principles soon follow. It requires a strong grasp of the fundamentals to get a shot at SOLID programming. The inherent flexibility in the language places the burden of responsibility on the programmer.

What I love the most is JavaScript empowers good programming. A good grasp of all the minutiae and fundamentals will take you far in any language. This approach is super important with callback functions in vanilla JavaScript. By necessity, all the nooks and crannies will take your skills to the next level.

Sponsors