Skip to main content

Command Palette

Search for a command to run...

29. ASYNC | Callback Hell in JavaScript

This article will talk about callback hell in javascript.

Updated
3 min read
S
Full-Stack Developer with 4 years' experience, specializing in backend development. Skilled in JavaScript, React, Python, Databases, and AWS. Known for building scalable web apps, leading teams, and maintaining strong client communication. Upskilling in Generative AI.

What is Callback Hell?

"Callback hell," also known as the "Pyramid of Doom," is a situation in JavaScript where multiple nested callback functions are used to handle a sequence of dependent asynchronous operations. This creates deeply indented code that is difficult to read, debug, and maintain.

Causes and Problems

The issue arises when an asynchronous task's execution depends on the result of a previous one. This forces each subsequent operation to be nested inside the previous function's callback, leading to:

  • Poor Readability: The excessive indentation makes the code's flow hard to follow.

  • Difficult Maintenance: Modifying or adding new features to the deeply nested structure becomes complex and error-prone.

  • Complicated Error Handling: Errors must often be handled at each nesting level individually, rather than in a centralized manner.

  • Inversion of Control: Developers hand over control of program flow to the callbacks, making the logic harder to reason about.

Example of Callback Hell

// Example 1
getUser(userId, (user) => { 
      getOrders(user, (orders) => { 
              processOrders(orders, (processed) => { 
                          sendEmail(processed, (confirmation) => {
                            console.log("Order Processed:", confirmation);   
                                   }); 
                            }); 
                }); 
});
// Example 2
function makeXHR(method, url, callback) { 
        const xhr = new XMLHttpRequest();
        xhr.responseType = 'json';
        xhr.addEventListener('load', () => {
                    callback(xhr.response)
        })
        xhr.open(method, url);
        xhr.send();
}

const baseURL = 'https://dummyjson.com';

makeXHR('GET', `${baseURL}/users`, (usersData) => {
    const userId = usersData.users[0].id;
    makeXHR('GET', `\({baseURL}/posts/user/\){userId}`, (postsData) => {
        const postId = postsData.posts[0].id;
        makeXHR('GET', `\({baseURL}/comments/post/\){postId}`, (commentsData) => { 
             const userId = commentsData.comments[0].user.id;
             makeXHR('GET', `\({baseURL}/users/\){userId}`, (userData) => {
                    console.log(userData); 
                    }); 
               }); 
          }); 
});

This structure forms a "pyramid" shape, leading to the term "Pyramid of Doom".

Solutions to Callback Hell

  1. Promises: Introduced in ES6, promises offer a cleaner, more structured way to handle asynchronous operations using .then() and .catch() methods. This approach allows for linear chaining of operations instead of nesting.
getUser()
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0]))
  .then(details => processOrder(details))
  .then(result => console.log(result))
  .catch(err => console.log(err));
  1. Async/Await: This syntax, introduced in ES2017, provides an even more readable, synchronous-like way to write asynchronous code. The await keyword pauses execution of the async function until a promise is resolved, and error handling is simplified with standard try...catch blocks.
async function process() {
  try {
    const user = await getUser();
    const orders = await getOrders(user.id);
    const details = await getOrderDetails(orders[0]);
    const result = await processOrder(details);
    
    console.log(result);
  } catch (err) {
    console.log(err);
  }
}

process();
  1. Modularization/Named Functions: Breaking down the logic into smaller, independent, named functions reduces nesting and improves overall code organization.
function handleUser(user) {
  getOrders(user.id, handleOrders);
}

function handleOrders(orders) {
  getOrderDetails(orders[0], handleDetails);
}

function handleDetails(details) {
  processOrder(details, handleResult);
}

getUser(handleUser);
  1. Callback Libraries: Libraries like async.js offer utility functions to manage asynchronous flows more effectively, though Promises and async/await are the standard native JavaScript solutions.

Happy reading!