What is callback hell? With JavaScript examples
When you are talking to JavaScript developers, they might complain about the pains with Callback hell. Then you might be wondering: “What the hell is callback hell”? In this article, I will try to explain what it is with some example code to go along with it.
In short, callback hell means that you have multiple functions that are asynchronous. Those functions depend on each other, which might, in turn, get quite messy with a lot of callback function that is nested in multiple layers. This will result in chaos, and you will end up with code which is hard to read and maintain.
If that didn’t make any sense, I totally understand. I will try to explain step by step.
What is a callback?
A callback is just a regular function, which is called after another function has finished executing.
Here is a simple example using the SetTimeout function in JavaScript.
console.log("Before...");
setTimeout(function() {
console.log("It took 500ms to get here!");
}, 500);
console.log("After...");
The setTimeout has two parameters. The first is an anonymous function, and the other is 500. The anonymous function is the callback function. The second function is how many milliseconds it should wait before running the anonymous function.
If you run the code above, you will get the following output:
Before...
After...
It took 500ms to get here!
First, it will log to console with “Before…”. Then it will start the setTimeout function and lastly “After…” will be written to console. This happens in an instant, but it takes 500 milliseconds until the callback function gets executed and “It took 500ms to get here!” gets logged.
The benefit is that you can do other work while the long-running task is executing.
What is the point of having the code asynchronous?
Running JavaScript logic in itself is really fast, like loops, in-memory calculations, etc. In those cases, your code can just be synchronous, meaning it just runs line by line in your code.
You want to make your code asynchronous when you are doing things that take relatively long time. These are mostly I/O tasks like:
- Making a request to a weather API.
- Getting a file from the disk.
- Getting data from the database.
- Downloading a file from the internet.
There is no reason that your application should stop when waiting for those kinds of tasks. Instead, you want to start the request and have a callback function that runs after the request is completed. That way your program can execute other stuff in the meantime, making your application seem faster for the user.
So, what the hell is callback hell?
Let’s start off with an example:
getDataFromDatabase(id, function(dataFromDatabase) {
getDataFromWeatherAPI(dataFromDatabase.url, function(dataFromWeatherAPI) {
saveDataToDatabase(dataFromWeatherAPI, function(result) {
console.log("am I in hell?")
});
});
});
Here we are doing the following:
- Get data from the database by using a database id.
- When data is retrieved from the database the callback function will be executed with data received from the database.
- We use the data from the database to call a weather API.
- After the data is retrieved from the weather API, we save the data to the database which triggers another callback function.
The callback functions are nested inside each other since we depend on the data from the previous function.
This is what is called callback hell. Be nesting the code in such a way, you will easily get lost, and your code will just be a gigantic mess.
So how can we solve it?
Promises to the rescue
A promise is a different way of dealing with asynchronous calls and was introduced in ES6.
When you are calling an asynchronous function which returns a promise, you are “promised” to get an OK (resolved), or an error (reject) some time in the future.
By using promises, we can refactor the previous example to look like this:
getDataFromDatabase(id)
.then(getDataFromWeatherAPI(dataFromDatabase.url))
.then(saveDataToDatabase(dataFromWeatherAPI))
.then(function(result) {
console.log("Ah, much nicer!")
})
.catch(function(error) {
console.log(error)
});
Instead of nesting the callbacks, we have chained the functions with the “then” keyword. This simply means when the promise resolves the asynchronous call, the next function will run.
We have also added a catch at the end of the chain. We only need one catch at the end, because if any of the promises gets rejected, the last catch block will be executed.
This way we won’t get that ugly nesting from the previous example.
How do we create a Promise?
Let us simplify the previous example by just calling the get database method and create a simple promise in it.
let gottenData = false;
let getDataFromDatabase = () => {
return new Promise((resolve, reject) => {
if (gottenData) {
resolve({ url: 'www.weatherapi/uk/london/' });
}
else {
reject("Error getting data");
}
});
};
We have just cheated a little here and created a fake database request.
Notice that I’m using ES6 syntax. Promises were first introduced out of the box in JavaScript in ES6. It can work on ES5 as well, but then we need to import an external library.
Back to the example, here we are declaring a function which returns a new promise. A promise has two parameters, resolve and reject.
Resolve is executed after the “database request” is finished and everything went fine. If there was an error, then reject will get executed.
Now we can execute the code the following way:
getDataFromDatabase()
.then((data) => console.log(data.url))
.catch((error) => console.log(error));
Here we got a nice then and a catch function. If you run the code like it is now, you will enter the catch function and get “Error getting data”. Why? Because we hardcoded the variable “gottenData” to be false. If we change that to true, We will resolve the URL from the data from our fictional request.
Async and await
Now there is a newer way of calling asynchronous functions. With ES7, the keywords async and await were introduced.
This is not a new concept. C# has used this for years. With async and await, the code will look very similar to plain synchronous code.
Let us refactor our example to use async-await instead. We are keeping the getDataFromDatabase function, but instead of using then and catch we will instead write this:
(async () => {
try {
let data = await getDataFromDatabase();
console.log(data.url);
}
catch (error) {
console.log(error);
}
})();
A few things to keep in mind:
- You need to use ES2017 for this to work. NodeJS supports this from version 7.6.
- You have to prepend the async keyword before the function.
- When you are calling the function which returns the promise, you need to prepend that function with await.
Now notice we are handling errors with a try-catch block. Again, this is common in languages like C#. You can have many asynchronous methods in the try block. If any of them gets rejected in the promise, the code will continue in the catch block.
Even though you have to use ES2017 for async await to work, there is actually pretty wide browser support. All the major browsers support it, except for Internet Explorer, which sadly is still in use today. To cover all the browsers you can use a tool like Babel to transpile the code.
Since we now have async and await. Are promises now obsolete?
Well, no. We are still using promises under the hood. Async and await is just syntactic sugar, which means it does the same thing, only the syntax is changed in such a way that makes it sweeter to read.
One reason to still use the promises syntax is when we have multiple asynchronous functions which work independently from each other. We can use the Promise.all() function. Here we can pass in multiple asynchronous functions at the same time. This means all the functions work in parallel and getting the job done faster.
Let’s say we have three functions. The first function takes 5 seconds to finish. The second function takes 2 seconds and the third takes 3 seconds. If you fire all up at the same time with Promise.all(), you only need to wait for the slowest to finish. So 5 seconds.
With async await, we need to wait for each function in sequence, meaning you have to wait 10 seconds.
So be aware if you are able to run code like this in parallel. This way your application can be more performant and more user-friendly.
Summary
In this post, we have explained what callbacks are and how that can lead to callback hell. Callback hell is just multiple callbacks which depends on each other, which makes the code very indented and in the end, look like a pyramid. This makes the code hard to read and it is easy to get lost. Error handling will also be a pain since you need to handle it in every callback function.
We can combat callback hell by using promises. Promises make it possible to chain all the callback functions, making it a flat structure. Error handling is much cleaner since we just need to have a “catch” function at the end.
Lastly, we talked about async and await. This is a nice syntax which makes your code look like ordinary synchronous code. Here we use try and catch blocks for error handling which is familiar to people coming from a Java or C# background.
One Comment
Comments are closed.