DEV Community

Cover image for Callback Hell and How to Rescue it ?
Nikhil Upadhyay
Nikhil Upadhyay

Posted on • Updated on

Callback Hell and How to Rescue it ?

For understanding the concept of callbacks and callback hell, I think you should know about Synchronous and Asynchronous programming in JavaScript(or any other language). Let's see a quick view on these topics in context of JavaScript.

Synchronous Programming

It is a way of programming in which you can perform only one task at a time and when one task is completed we move to another task. This is what we called Blocking Code operation because you need to wait for a task to finish to move to the next one.

console.log("Program Starts");
let sum = getSum(2,3);
console.log(sum);
console.log("Program Ends");
Enter fullscreen mode Exit fullscreen mode

In the above code snippet, you see code will execute line by line and when an operation on one line is finished then we move to the next line so this is just a simple example of the synchronous way of programming and we do this in our daily life of programming.

Asynchronous Programming

Asynchronous programming allows you to perform that work without blocking the main process(or thread). It’s often related to parallelization, the art of performing independent tasks in parallel, that is achieved by using asynchronous programming.
In asynchronous operation, you can move to another task before the previous one finishes, and this way you can deal with multiple requests simultaneously.
In JavaScript, a good example of asynchronous programming is setTimeout function, let's see a quick example -

console.log("Program Starts");
setTimeout(() => {
  console.log("Reading an user from database...");
}, 2000);
console.log("Program Ends");
Enter fullscreen mode Exit fullscreen mode

So, the output of this program will look like -

Program Starts
Program Ends
Reading an user from database...
Enter fullscreen mode Exit fullscreen mode

Pretty cool, right? Our program didn't wait for setTimeout to finish, just goes for the next line, then came back to the function and prints the output. This is what we called Non Blocking code. You can read more about it here.
There are three design patterns in javascript to deal with asynchronous programming -

  • Callbacks
  • Promises
  • async/await (just a syntactical sugar of promises)

Callbacks

Callbacks is a great way of handling asynchronous behavior in javascript. In JavaScript, everything behaves like an object so functions have the type of object and like any other object (strings, arrays, etc) you can pass functions as an argument to other functions and that's the idea of callback.

function getUser(id, callback) {
  setTimeout(() => {
    console.log("Reading an user from database...");
    callback({id: id, githubUsername: 'jerrycode06'});
  }, 2000);
}

getUser(1, (user) => {
  console.log("User", user);
})
Enter fullscreen mode Exit fullscreen mode

You see, we are passing function as an argument to getUser function and it calls inside the getUser function, output will look like -

Reading an user from database...
User {id: 1, githubUsername: 'jerrycode06'}
Enter fullscreen mode Exit fullscreen mode

Callback Hell

In above code snippet, we are getting user with github username now let's suppose you also want repositories for that username and also commits in the specific repository so what can we do with the callback approach -

getUser(1, (user) => {
  console.log("User", user);
  getRepositories(user.githubUsername, (repos) => {
    console.log(repos);
    getCommits(repos[0], (commits) => {
      console.log(commits);
      // Callback Hell ("-_-)
    }
})
Enter fullscreen mode Exit fullscreen mode

You are seeing now a nesting of functions here and code also looks scary and this is what we called Callback Hell. For a big application it creates more nesting.
Callback Hell
To avoid this, we will see now Promises.

Promises

Promises are the alternative to callbacks for delivering the results of asynchronous computation. They require more effort from implementors of asynchronous functions, but provide several benefits for users of those functions. They are more readable as compared to callbacks and promises has many applications like fetch in javascript , mongoose operations and so on. Let's see how to implement promises with above example. Actually, promises have four states -

  • fulfilled - The action relating to the promise succeeded
  • rejected - The action relating to the promise failed
  • pending - Hasn't fulfilled or rejected yet
  • settled - Has fulfilled or rejected Promises First we have to create promises to understand this -
function getUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Reading from a database....");
      resolve({ id: id, githubUsername: "jerrycode06" });
    }, 2000);
  });
}

function getRepositories(username) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`Extracting Repositories for ${username}....`);
      resolve(["repo1", "repo2", "repo3"]);
      // reject(new Error("Error occured in repositories"));
    }, 2000);
  });
}

function getCommits(repo) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("Extracting Commits for " + repo + "....");
      resolve(["commits"]);
    }, 2000);
  });
}
Enter fullscreen mode Exit fullscreen mode

We created three functions, instead of passing callback function we are now returning a Promise which has two argument resolve and reject. If everything worked, call resolve otherwise call reject. Let's see how to use promises -

// Replace Callback with Promises to avoid callback hell
getUser(1)
  .then((user) => getRepositories(user.githubUsername))
  .then((repos) => getCommits(repos[0]))
  .then((commits) => console.log("Commits", commits))
  .catch((err) => console.log("Error: ", err.message));
Enter fullscreen mode Exit fullscreen mode

More readable, Isn't it? Using arrow functions made this less complex than using simple functions. We have avoided nesting of functions and reduce the complexity of code (callback approach) and that's how promises work. You can deep-dive more about promises here.

async/await

It is supposed to be the better way to write promises and it helps us keep our code simple and clean.
Aync-Await
All you have to do is write the word async before any regular function and it becomes a promise. In other words async/await is a syntactical sugar of using promises it means if you want to avoid chaining of then() methods in promises, so you can use the async/await approach but internally it also uses the chaining.
Let's see how to implement it with above example -

// Async- await approach
async function displayCommits() {
  try {
    const user = await getUser(1);
    const repos = await getRepositories(user.githubUsername);
    const commits = await getCommits(repos[0]);
    console.log(commits);
  } catch (err) {
    console.log("Error: ", err.message);
  }
}

displayCommit();
Enter fullscreen mode Exit fullscreen mode

Now, it is more readable than using promises above. Every time we use await, we need to decorate this with a function with async. Like promises, we don't have catch() method here so that's why we are using try-catch block for the error handling.

Conclusion

In this article we've seen -

  • Synchronous vs Asynchronous
  • Callbacks and Callback Hell
  • Avoid Callback hell with promises and async/await

I personally like the async/await approach the most but sometimes we should the promises approach to deal with async behaviour.

Thanks for reading this long post! I hope it helped you understand these topics a little better. If you liked this post, then please do give me a few ❤️ and share it if you can. You are welcome to
give any suggestions in comments and ask anything!

Oldest comments (10)

Collapse
 
farazkhanfk7 profile image
Hasan Faraz Khan

Great piece :-)

Collapse
 
jerrycode06 profile image
Nikhil Upadhyay

Thanks ;-)

Collapse
 
mukuljainx profile image
Mukul Jain

Nice article, check this out npmjs.com/package/await-handler
Using this we can skip the try catch block and handle error there it self. Like in case you want to show different error message for different errors, one way to go.

    let [err, result] = await on(myAsyncTask());
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jerrycode06 profile image
Nikhil Upadhyay

Nice, Thanks for sharing this!!

Collapse
 
oddward profile image
Mugtaba G

Awesome piece~ Thanks for the detailed yet clear and practical post

Collapse
 
jerrycode06 profile image
Nikhil Upadhyay

Glad you liked it.

Collapse
 
andreidascalu profile image
Andrei Dascalu

"I think you should know about Synchronous and Asynchronous programming in JavaScript(or any other language)"

Except that no ... the three patterns of Async programming you describe don't apply to any other language except Javascript.
While ReactPHP has some things that resemble callbacks, just about any other async-capable language uses multithreading, either soft threads (managed by the platform such as Go's goroutines or PHP Swoole's coroutines), system threads or full-blown parallelism.

For a fully relevant discussion on async programming, it might be better to start with concurrency vs parallelism and then see what JS is capable of.

Collapse
 
jerrycode06 profile image
Nikhil Upadhyay

This article basically focuses on callback hell and I have given just a quick overview of async programming, although I agree on your point -
"the three patterns of Async programming you describe don't apply to any other language except Javascript."
Thank you for mentioning it, I should've added it to my article.

Collapse
 
aman-godara profile image
Aman Godara

Thanks, enjoyed reading it!

Collapse
 
sidhunishan786 profile image
Nishan Singh

Nicely written!!