What is Callback Hell in JavaScript?

Learn via video course
FREE
View all courses
JavaScript Course With Certification: Unlocking the Power of JavaScript
JavaScript Course With Certification: Unlocking the Power of JavaScript
by Mrinal Bhattacharya
1000
4.8
Start Learning
JavaScript Course With Certification: Unlocking the Power of JavaScript
JavaScript Course With Certification: Unlocking the Power of JavaScript
by Mrinal Bhattacharya
1000
4.8
Start Learning
Topics Covered

In software engineering, the term callback hell generally refers to an ineffective way of writing code asynchronously. It is an anti-pattern that is often called the “pyramid of doom”. Now, before we start learning about callback hell in JavaScript, there are certain terms that we need to understand, which will be covered next.

What is a Callback?

In JavaScript, everything (strings, arrays, functions) is considered an object. Hence, the concept of callbacks lets us pass functions as arguments to another function which will be executed later within the outer function.

Next, let’s discuss the importance of callbacks in JavaScript.

Assume we have a function called 'compute'. It takes in 3 parameters namely a string and two numbers. The string ‘action’ is compared through a series of if conditions and the desired operation is computed and returned as shown below.

This works fine, but what if we need to add a third or fourth operation? As the requirement increases, the number of if conditions inside the 'compute' function will also keep increasing which will lead to a messy code implementation as shown below.

Hence, we can rewrite the code in a better way as shown below.

Here, for each operation, we have defined its own function body independently. In this way, we can pass the functions as callbacks to the 'compute' function. Hence, if we want to add a new operation, say 'add', we first need to create a separate function for 'add' and then pass it as an argument in the compute function thus making it easier to understand.

The most common examples of callback functions in JavaScript are addEventListener, array functions (filter, map, reduce) etc.

Before we move ahead, let's understand the difference between pass-by-value and pass by reference in JavaScript.

JavaScript offers two sets of data types such as primitives and non-primitives. Primitve data types such as null, undefined, boolean, and string cannot be changed (immutable) while non-primitive data types include objects such as arrays and functions that can be changed (mutable).

Now, while passing by reference, we basically pass the address (reference) of the entity such as a function as an argument to the function being invoked. Hence, if the value of the argument inside the function is changed, then it correspondingly alters the original value found outside the function. On the other hand, passing an argument by value into the function will not change its value outside the function because it copies the value into two different locations in memory hence treating them as entirely separate entities. In JavaScript, all objects interact by reference.

In addEventListener, it listens for an event such as a click event, and accepts the second argument as a function that is to be executed once the click event is triggered. This function is passed as a reference without any parentheses. In the example shown below, the display function is passed as an argument into the addEventListener as the callback which later gets invoked when the click event happens.

Similarly in array functions, if we take the filter function as an example, it can take in another callback function as an argument. In the example shown below, largeNum is the callback function passed onto the fiter function on array arr.

Synchronous JavaScript

In general, synchronous programming has the following two features :

  1. A single task is executed at a time.
  2. The next set of following tasks will only be addressed once the current task gets finished. Thereby following a sequential code execution.

This style of programming gives rise to a “blocking code” situation as each task has to wait for the previous tasks to get done.

Now, when it comes to JavaScript, we often get confused about whether it’s synchronous or asynchronous.

In the above example for the filter function, the callback function gets executed inside the filter function synchronously. Hence, it is called a synchronous callback. The filter function has to wait for the largeNum callback function to finish execution. Hence, the callback function is also called blocking callbacks as it blocks the execution of the parent function in which it was invoked.

The prime definition of JavaScript claims that it is single-threaded, synchronous and blocking in nature. But interestingly, we can make it behave asynchronously based on our use cases.

Asynchronous JavaScript

The asynchronous style of programming focuses more on improving the performance of the application and callbacks can also be used in such situations. The asynchronous behavior of JavaScript can be explained using the setTimeout function as shown in the code below.

As shown above, the setTimeout function takes in a callback and time in milliseconds as arguments. The callback gets invoked after the time mentioned (here 1s). In short, the function has to wait for 1s for its execution. Now, to give more clarity observe the code below.

Here, the code written after the setTimeout function gets executed while the timer function waits for 1 second to pass. Hence, firstly, “One” gets printed, secondly “Two” gets printed and after 1 second the timer function invokes the callback function and prints “Timer displayed after 1 second”.

To understand the asynchronous behavior of JavaScript while using the setTimeout function we need to get familiar with the event loop. This might be beyond the scope of our article but we will give you a brief insight.

Our web browser consists of a JavaScript engine, web APIs, local storage, timers, etc. JavaScript engine contains a single call stack in which all the code is executed immediately as and when it is pushed without waiting.

Now, we need to understand that the setTimeout function is not a part of JavaScript but it is a part of the web browser as it is essentially a web API and the browser allows the JavaScript engine to access the setTimeout function with the help of the global window object.

Next, we need to understand that along with the call stack, the JavaScript engine also consists of a callback queue for dealing with asynchronous events easier.

Call Stack and a heap memory in JavaScript Engine

As depicted in the above image , the JavaScript engine consists of a call stack and a heap memory. The call stack is the spot where all the code gets executed then and there without waiting as and when they get pushed. On the other hand, heap memory is the spot where the engine allocates memory for objects and functions at run time as and when it is required. As mentioned before, our browser contains web APIs such as setTimeout, DOM APIs, console, fetch etc. and the engine can access these APIs using the global window object. Next, we have the event loop which sort of serves like a gatekeeper that picks functions such as setTimeout inside the callback queue and pushes them onto the stack as they require a certain amount of waiting time.

In the above example of the setTimeout function, when the function gets encountered, the timer gets registered in the callback queue. Next, the rest of the code gets pushed as usual into the call stack and gets executed. When the timer expires, the callback queue then pushes the callback function which was registered in the setTimeout function into the call stack and it thereby gets executed after the specified time.

Callback Hell

The phenomenon which happens when we nest multiple callbacks within a function is called a callback hell. The shape of the resulting code structure resembles a pyramid and hence callback hell is also called the “pyramid of the doom”. It makes the code very difficult to understand and maintain. It is most frequently seen while working with Node.js. The code below shows an example of a callback hell.

To understand the internal working of callback hell in javascript, assume we have to achieve a task say D. To achieve task D, we need to execute a series of dependent tasks ranging from A to C asynchronously. In short, first we execute task A, we get the result and task B need to depend on result A to start its execution. Similarly, unless result B is produced we can't execute task C. This leads to the chaining of tasks from A to D, which creates a set of nested callbacks.

In the example shown below, getUserData takes in an argument that is dependent or needs to be extracted from the result produced by getArticles which is inside user. The same dependency can be observed with getAddress also. This scenario is termed callback hell.

Now let's discuss various ways to escape the callback hell in javascript.

Promises

A promise is an object with a syntactical coating introduced by ES6 which became another way of skipping writing callbacks to implement asynchronous operations. Web APIs like fetch() are implemented using the promising methodology. It makes the code more readable and is a solution to escape the callback hell.

Promises generally have three states :

  1. Fulfilled: the desired action has been resolved or completed successfully.
  2. Pending : the desired action has neither been resolved nor been rejected and still in its initial state.
  3. Rejected : the desired action has been rejected causing the desired operation to fail.

Unlike synchronous functions, asynchronous functions may not return a value immediately. Hence, these asynchronous functions instead return a Promise() which can be later used to get the final value when it gets available in the future. We can easily handle situations where we successfully receive the data using the resolve() or where we fail to get the data due to some network error, timeout etc using the reject() as shown below.

The code below shows an example of how promises are written. If we observe we can see that it reduces the complexity of the code by avoiding the nesting of callback functions we saw in callback hell. Promises offer us .then() and .catch() to handle operations that gets completed or fails. The two examples shown below illustrate the same.

This is an example of writing a promise.

In the above example, to give the getArticle function a promise functionality, we return a new Promise(). Now, when we invoke the function by passing an id equal to 1, a new promise is returned by the function. Next, the timer waits for 5 seconds and prints Fetching data.... on the console. Since in the function invocation we have chained a .then() constructor, we can show the resolved output as a response. Hence, the console displays {id: '1', name: 'derik'} immediately.

This is another example showing a more practical example.

In the above example, let us assume that the functions getArticles, getUserName, getAddress are functions executing something similar to making an API request that returns a promise. So, first, we pass 10 as an argument to getArticles which returns us a promise. When this promise gets resolved, the desired data is extracted from the resolved data and passed as an argument in the getUserName function. The same process is repeated with the getAddress function also. Finally, once we get the final data, we print it in the console. So far, these steps involved chaining with the then() while if the promise in the final function call gets rejected, to address that scenario, we have chained it with a catch() where the error will be printed on the console if the promise gets rejected.

Async/Await

This is the second method to escape the callback hell. It serves as a better way of approaching promises because we are removing the usage of .catch() and .then() which again depends on callbacks. Async/await acts as a syntax sugar built on top of promises. It internally resolves promises as well as makes it easier for developers to write readable code. But it cannot be used with normal callbacks. It can be carried out by adding an ‘async’ before the function keyword. Though internally it creates chaining, it’s better than using promises as it enhances the understandability of the code and also prevents code blocking.

Since we are not using the .catch() (like in promises), we can write the async/await statements inside a try/catch block which is a better way of error handling. This is frequently used for asynchronous applications such as fetching data from an API etc. Avoiding the .then() and .catch() methods makes debugging easier for developers as well.

The await keyword must be written inside a async function and it essentially waits for a Promise(). The async function is made to stop its execution until a Promise() is either fulfilled or rejected. The function will resume only once the promise is settled. After it resumes, the value of the set of await expression will contain the value of the settled Promise(). await expression shows the rejected result if the Promise() gets rejected.

The following table describes the advantages and disadvantages of using async/await.

AdvantagesDisadvantages
Improves the performance of web applications especially when long running tasks are involved.async/await won't run on old browsers or environments.
Enables us to write less code as compared to Promises.Can be difficult for beginners to understand the underlying concept.
Helps us to write readable and maintainable code

Escaping the Callback Hell

In a nutshell, the usage of promises and async/await serves as a way to escape the callback hell as mentioned above. Apart from these writing comments and splitting the code into separate components can also be tried out. So, currently most the software engineers prefer using the async/await while building applications.

Conclusion

To sum up,

  • Functions that are passed as arguments to other functions and executed later inside the outer function are called callback functions. We commonly use them in addEventListener, and array methods like filter, map, etc.
  • JavaScript is a single-threaded, synchronous language. Synchronous callbacks are blocking in nature. For example, the array methods have to wait for the callback function to finish for them to finally complete their execution.
  • On the other hand, JavaScript can be made to behave asynchronously. For example, we can observe the asynchronous behavior of JavaScript by implementing the setTimeout function provided by he Web the APIs.
  • Callback hell is a phenomenon that happens when multiple callbacks are nested on top of each other. The two common ways of escaping the callback heare are by using promises and async/await.
  • Promises mainly have three stages such as resolved, rejected, and pending. It makes the code more maintainable and understandable.
  • Async/await is a better way than promises to resolve the callback hell situation. By removing the .then() and .catch() methods of promises, it uses async/await keywords which not only reduces the code complexity but also makes the error handling easier as it can be written within a try/catch block.