What are callbacks, and what is ‘callback hell’ in JavaScript?

Last updated: September 27, 2022.

Callbacks are a way of structuring code so that a function will wait for a previous one to complete before executing.

In JavaScript, this is important because JavaScript doesn’t wait for code that it recognizes may take a long time to complete. Instead, it starts executing it, but only handles its result after it has executed the rest of a script. This type of code that JavaScript puts to one side is called asynchronous code. Typical asynchronous code is any type of HTTP request or API call and setTimeout().

Handling this type of code may give you a headache. But it is actually a good thing: it means JavaScript is efficient in executing an entire script as quickly as possible.

But what if you want to handle the result of some asynchronous code? One option for doing this is to use callback functions.

Table of contents

Callback functions you may have already written

Though more modern methods for handling asynchronous code are now available, callbacks are still everywhere in modern JavaScript. You may even have dealt with them without knowing it!

For example, have you ever added an event listener to an element, passing in a function to the second position? This is a callback function. It fires only after a click on the element occurs.

element.addEventListener('click', function() {
    console.log("You clicked the element!");
})

Another really common is example is in using setTimeout(). The argument in the first position executes only after the timer has run down.

setTimeout(function() {
    console.log("Two seconds is up");
}, 2000);

If you have passed in a function to either of these methods, you have already written a callback function!

Next, let’s take a look at how you can configure a function you have created yourself to accept a callback function.

How to make your own functions accept callbacks

In the following code, task2 will log to the console before task1 because of the setTimeout() inside task1.

function task1() {
  setTimeout(function() {
    console.log("Task 1 complete!");
  },2000)
}
 
function task2() {
  console.log("Now task 2 can begin");
}

task1()
task2()

We can solve this so task2 is only executed after task1 is complete by configured task1 to accept a callback function.

To do so, we add a callback parameter to task1 and call it after the log to the console.

function task1(callback) {
  setTimeout(function() {
    console.log("Task 1 complete!");
    callback()
  },2000)
}
 
function task2() {
  console.log("Now task 2 can begin");
}

task1()
task2()

So now, when task1 is called, we have the opportunity to do something after the log to the console. But this isn’t defined yet.

The thing to do after the log to the console is defined when we call the task1 function.

task1 accepts an argument that is a function to be called after the log to the console. This is the callback function. In this case, we call task2 in this way:

function task1(callback) {
  setTimeout(function() {
    console.log("Task 1 complete!");
    callback()
  },2000)
}
 
function task2() {
  console.log("Now task 2 can begin");
}

task1(function() {
   task2()
})

Using this pattern, you can execute many tasks in order.

Below, four functions are executed in order from within the main function:

function task1(callback) {
  setTimeout(function() {
    console.log("Task 1");
    callback()
  },2000)
}
 
function task2(callback) {
  setTimeout(function() {
    console.log("Task 2");
    callback()
  },2000)
}

function task3(callback) {
  setTimeout(function(callback) {
    console.log("Task 3");
    callback()
  },2000)
}

function task4() {
  console.log("Task 4");
}

function main() {
    task1(function() {
        task2(function() {
            task3(function() {
                task4();
            }) 
        })
    })
}

main();

The above code calls the next task function until another one begins.

But notice that the executing code inside the main function is becoming. This is what some developers refer to as ‘callback hell’.

Callbacks: modern usage

There are now modern alternatives to callbacks, such as promises and async/await, that solve the ‘callback hell’ problem. However, callbacks are still extremely common in Javascript programming. You may have even used them without realising using the following methods:

  • The addEventListener methods takes two parameters: (1) an event and (2) a callback function.
  • The setTimeout method taskes two paramter: (1) a callback function and (2) a specified time delay in milliseconds in executing it.
  • The setInterval method taskes two paramter: (1) a callback function and (2) a specified time interval in milliseconds between repeatedly executing the callback function.

Conclusion

Callbacks are extremely useful because they help us deal with Javascript’s asychronous behaviour: we can tell Javascript to only execute some code only after it has finished executing some prior code. This functionality is crucial when developing modern websites and apps that often retrieve and send data to a backend database/server.

Despite modern alternatives, callbacks are still used commonly in Javascript programming. Most often, they are used for simply scheduling of one action after another. However, when scheduling becomes more complex, it is better to rely upon the modern alternatives of promises and async/await than find yourself in so-called ‘callback hell’.