JavaScript Promises: Avoid the Callback Hell

In my day to day work I found myself engaged in discussions whether or not we should use JavaScript promises to solve a particular use case. Do they add more complexity or do they bring clarity and sense to the JavaScript soup? As a warning to the reader, this blog post is not about explaining in very details what a promise is or how a promise can be implemented. You can find some good reading materials about these topics below in the references section. As it is important to understand how something works behind the scenes before you can use it effectively. Otherwise, you are just remembering strange rules that you do not understand and decisions like “Should we use promises?” could take random turns. This blog post is more about “Why should we care about promises?”, explaining the basic concepts behind the Promise pattern and the Callback pattern so that you can make an informed decision when to use which. However, it is slightly opinionated and leads the reader towards using promises over callbacks when the complexity and the size of the code base grows.

What is a promise?

A promise is an abstraction dealing with the asynchronous nature of the JavaScript code. It represents the notion of a future value wrapped inside an object. You no longer have the need to deal with time, i.e. when the result of an operation will be ready to use.

Why do we need promises?

Promises can be difficult to understand for people coming from the synchronous world where functions simply return a value or throw an error. Well, for good or bad, in the JavaScript world you rarely could afford to have such a flow. JavaScript works with callbacks, i.e. execute this function with the result from the previous one. In a larger code base that could easily turn into unreadable callback hell. If you have started your career as a backend developer, like I did, you might want to look for patterns that will bring sanity to your JavaScript code and make it read naturally from top to bottom. Do not worry! JavaScript promises will save you from the callbacks hell. Let’s have an example.

Synchronous code

var user = registerUser();
// use the user object here

Asynchronous code

registerUser(function(user) {
  // use the user object here
});

Asynchronicity comes in when your code needs to perform a lengthy task in a non-blocking way. For example, calling an external API with AJAX does not block your main thread but it returns immediately and the execution flow continues. Then your callback is called with the response as an argument once that remote call has completed. With a single callback both approaches are fine. But when you have a callback calling a callback calling a callback … calling a callback, then you enter the callback hell nesting callbacks within callbacks. Let’s go through a more complex example.

The Pyramid of Doom

registerUser(function(user) {
  sendActivationMail(function(user) {
    provisionToLegacySystem(function(user) {
      notifyMarketingToStartTheSpam(function(user) {
        showThankYouPage(user);
      });
    });
  });
});

The way of the JavaScript promise

registerUser.
  then(sendActivationEmail).
  then(provisionToLegacySystem).
  then(notifyMarketingToStartTheSpam).
  then(showThankYouPage);

You can easily spot the difference. By using promises you can turn that nested code into a clean poem that reads naturally from top to bottom. We have removed the time variable from our equation. All of our worries about “When that value will be ready?” are far gone now. The good thing about promises is that each promise returns a promise. So you can nicely chain them as shown in the example above and handle errors in one place for the whole chain.

What is wrong with callbacks?

1. No clean way to pass the result back to the caller

Let’s go back to our example from above. We have an asynchronous function calling another asynchronous function and so on. The caller is a few layers away from the last callback that produces the final result. If the top caller needs the result from the execution of the bottom callback then you are in trouble as you can hardly achieve that using the Callback pattern. Your overall application design will suffer because you have to build in that callback functionality into every asynchronous method. Such a design would rarely produce a readable code base.

2. No clean way to perform decent error handling

Error handling can be a real pain when you have multiple layers of nested asynchronous functions. You want to handle the error at the top of the callback chain. The poor callbacks down the execution chain have no idea who called them and why in order to provide a decent error handling. The error has to fight its way back to the top caller as it is the only one who knows the big picture and can decide on how to act. In contrast when using promises you do the error handling in one place for the whole execution chain by simply chaining .catch after the last .then.

3. No clean way to execute a finally block

Imagine you have a number of nested asynchronous functions. Each of them could fail or not but you still need to clean up state or resources at the end. The problem is that you do not know where the end is as each callback knows nothing about how many asynchronous methods lie ahead of it and how many could possibly lie after it. If they know, that makes them tightly coupled into a single workflow and changing that workflow will result in changing those functions. For example, you have started a spinner to indicate a long-running operations to the user and at the end you want to hide that spinner. You could do some magic to achieve that with nested callbacks but you will most probably end up mutating a global state and injecting the same error handling code into each asynchronous function. That is far from clean. In contrast when using promises you simply put one finally at the end of the promise chain and you are done. Clean and simple.

4. Callback pattern abuse

Developers tend to abuse the callback pattern inlining anonymous functions within their code. Such code can be a nightmare to read, understand and maintain. Imagine you have to extend your code by adding one more asynchronous function somewhere in the middle of that soup. Then you have to wrap somehow everything after that point into a new callback and pass the result from the asynchronous method to it alongside with all the context as it is before your change. In contrast when using promises that would result in simply adding one .then() to the promises execution chain. Of course, anyone who got that power can also abuse the Promise pattern by inlining fat anonymous function and making the promise chain impossible to locate and follow along. But still the promises remove the deep nesting of callbacks and more or less force the developer into decomposing behaviour into clear isolated functional pieces.

5. Flat is better than nested

No, no, no! It is not because of Python or Zen. But because it is way better to have a hierarchy of level one or two rather than five or higher. Putting all fights aside we all have to agree that it improves readability. Reading code from top to bottom feels natural. Looking up nested callbacks trying to figure out what the flow is and where it ends is probably something you won’t enjoy doing on a daily basis. In short: flat code is simpler than nested code. Do prefer writing simple code as bugs have difficult time hiding inside simple code.

Callback pattern vs Promise pattern

The Callback pattern is an Inversion of Control (IoC) pattern. You do not call a method to get a return value. Instead you are handling control over to the callee saying: “Hey, here is my callback. Call me with the result when you are done.” The Promise pattern inverts the inverted control back to you. It allows you to write asynchronous code synchronously removing the nesting and making it much more readable. The Promise removes the responsibility of building in that callback functionality when designing an asynchronous function. It helps you organize your callbacks into readable and maintainable actions. It makes it easier to handle errors and execute a clean up block at the end. As a developer it is up to your to choose which path to follow. But understanding the pros and cons of both patterns will give you the confidence that you have chosen well when solving problems.

Takeaway

NOTE: If your code is hard to read due to nesting of callbacks then use promises to clean it up.

Year 2017 Update

The new way of fighting callback hell is async/await functions. The “promise” pattern mostly emerged to circumvent limitations in the JavaScript language of that time. Today with ES2017 the language itself is mature enough to help you avoid callback hell without chaining promises with then.

References