Using Callback Composition to Avoid Javascript’s Callback Hell

Use this pattern to fight back Javascript’s Callback Hell

Lucas Pereyra
4 min readMar 20, 2023

Welcome to my very first article of 2023, hope this is a great year for us all! This time I bring up an interesting pattern I’ve come up with to address the complex problem of Javascript’s Callback Hell. I hope you find it interesting and apply it to your projects as well, let’s get into it.

The Javascript Callback Hell

Asynchronous programming in Javascript has been mostly driven by two approaches: the callback model approach and the Promise approach. The callback model imposes the use of callbacks as handlers to specify what’s going on next once a function has finished executing and its outcome is ready to use. On the other hand, the Promise approach basically consists of chaining Promises to define a dependency chain in which each one’s output will be the next’s input; using the then and catch methods.

The callback model is often encouraged by common libraries that don’t use Promises. We need to specify a handler callback which mostly consists of a function that receives two arguments: an error instance -holding the error occurred in case something went wrong- and the output produced by the function. This contract might vary according to its implementation but is often more less the same. The following is an example of the native NodeJS fs module that uses this model:

The Promise model entails chaining Promises to define a chain of asynchronous dependencies. Each Promise is then executed after the previous has been resolved. This pattern can lead to cumbersome chaining code if the Promises involved aren’t as simple as the ones shown here:

Issues related to the Promise model have been mostly resolved with the rise of async/await in ES6. Nevertheless, the callback model is still causing headaches to Javascript developers. Take a look at the following snippet:

Functions f1, f2 and f3 carry out very simple tasks and rely on the callback model. If we are to put them all together, we’ll have to chain them all, manually crafting the needed callbacks for each case. Wouldn’t be good to get rid of this callback hell of nested function calls and their handlers?

Using callback composition

Certainly, we can’t get rid of the previous exhibited code, but we could automate the process of creating the callback chain manually. From the last example, we can tell there’s a form of a pattern going on, that describes how those callbacks are built in relation to each other. My intention is to generalize this pattern in a helper function that carries out callback composition for us. This helper would receive the callbacks that are to be composed as its arguments, and would return a single general callback to be used. It would look as follows:

Nice, we’ve defined the interface for our compose helper function:

  • receives a set of callbacks that comply with the signature function(inputValue, function(resultValue, errorValue))
  • returns a callback that complies with exactly the same signature, and is composed of the passed in callbacks

Working out this helper isn’t an easy task, I winded up spending much more time than I thought. You can find my last working version of this helper in perlucas/javascript-compose. Feel free to use, extend and improve this code as you need.

Examples

This compose helper allows us to work off the callback hell issue by proposing a more declarative syntax:

We can pass in async functions to compose as well:

By adding a surrounding try/catch block to the resulting composed function, within the compose helper, we can safety handle all errors thrown up by callbacks, without any effort:

Conclusion

This helper is a nice attempt to minimize or at least confine the callback hell issue from spreading throughout the code. As long as a function complies with the established interface function(input, function(result, error)), it will be perfectly composable. Refer to perlucas/javascript-compose for more insights about its usage.

Not only this helper automates the effort of manually chaining callbacks, but also enforces decoupling of callbacks by abstracting from us the low level details about how the resulting chain is constructed. As long as we provide callbacks that follow the required interface, we can compose plenty of functions and come up with a chain as complex as we want.

Feel free to adapt this solution to fit your specific needs. Perhaps you need a slightly variation of the proposed interface, or a different error handling strategy. It is tough to come up with a general solution that satisfies all possible scenarios, so don’t hesitate in making the necessary adjustments.

While this very first version meets the need of using async functions as composable callbacks, it lacks any support for Promises to be fed in as composables. For now I decide to keep it that way, it shouldn’t be a problem as Promises can be easily translated into async functions.

We’ve met the end of this article, hopefully it was of great utility for you! Thanks a lot for your reading, feel free to explore my previous articles and stay connected for more interesting programming content.

--

--

Lucas Pereyra

Software Engineer specialized in backend development, performance optimizations and abstraction-guided development. Enjoy my writings and let's connect!