JavaScript Promises: Avoid the Callback Hell

Clean Code JavaScript

I often feel some bad air surrounding JavaScript promises. Many fail to truly understand them and avoid the topic. Some truly understand them but cannot explain them and avoid the topic. Others truly understand them and can explain them but have doubts when to use them and when not to.

Do promises add more complexity or do they bring clarity and sense to the JavaScript soup? As a note 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. 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.

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 at all?

Promises can be difficult to understand for people coming from the synchronous world where you simply invoke functions one after the other and the execution flows nicely from top to bottom. Well, for good or bad, in the JavaScript world you could rarely afford to have such a natural flow. You cannot just stop the world and wait for an AJAX call to finish. This is not how the JavaScript engine works. JavaScript works with callbacks. When the response from that AJAX call has arrived, then invoke my callback function passing the response object as an argument.

In a larger code base that could easily turn into unreadable mess. You just cannot follow the program flow. You jump forth and back from a callback to a callback until you forget where you have started initially and what the heck you were doing in the first place. If you have started your career as a backend developer, as 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. Look no further! JavaScript promises to the rescue. Let’s see how.

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

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