What Is a Callback?
A callback is simply a function that is passed as an argument to another function, and then invoked (called) inside that function.
This concept is possible because functions in JavaScript are first-class citizens. This means:
- Functions can be stored in variables
- Functions can be passed as arguments to other functions
- Functions can be returned from other functions
Because of this flexibility, JavaScript heavily relies on callbacks to control execution flow.
Why Callbacks Exist
Callbacks allow one piece of code to say:
“I don’t know when I’ll be ready, but when I am, please run this function.”
This idea is useful in both:
- Synchronous scenarios, where execution happens immediately
- Asynchronous scenarios, where execution is delayed (for example, file reading, timers, or network calls)
Callbacks in Synchronous Code
Let’s start with a simple and very common example: using a callback with an array method such as forEach.
const numbers = [1, 2, 3, 4];
function printNumber(num) {
console.log(num);
}
numbers.forEach(printNumber);
When we use forEach, we pass a function as an argument. That function is the callback.
Important observations:
- The callback is not executed immediately when it is passed.
- Instead, it is invoked internally by the
forEachmethod. - The callback runs once for each element in the array.
- Everything happens synchronously, in order.
Even though a callback is involved, nothing asynchronous is happening here.
This demonstrates an important idea:
Using a callback does NOT automatically make code asynchronous.
Callbacks in Asynchronous Code
Now let’s look at a different scenario — an asynchronous function.
Imagine a function that performs an operation like reading a file or waiting for a timer.
Such operations take time and cannot block the main thread.
function asyncOperation(callback) {
setTimeout(() => {
callback();
}, 0);
}
asyncOperation(() => {
console.log("Callback executed");
});
console.log("Hello World");
In this case:
- A callback is passed into the function.
- The function starts an asynchronous task.
- The callback is not executed immediately.
- Instead, it is placed into the message queue once the task finishes.
While the asynchronous operation is running, JavaScript continues executing the remaining code.
Only when the call stack becomes empty does the event loop move the callback from the message queue into the call stack.
This is why asynchronous callbacks appear to run “later”.
Why Order Changes in Asynchronous Code
When you run code that includes:
- A function call with a callback using
setTimeout(or similar) - A regular
console.logafter it
You will notice that the console.log runs before the callback.
This happens because:
- The callback is placed in the message queue
- The call stack must first become empty
- Only then can the callback execute
This behavior often confuses beginners, but it becomes very logical once you understand how the event loop works.
Important Insight
A callback can be:
- Synchronous, when it is executed immediately within the call stack
- Asynchronous, when it is deferred and executed later via the message queue
The difference is not the callback itself, but how and where it is executed.
Pros of Using Callbacks
Let’s begin by understanding the advantages of callbacks and why they became so widely used in JavaScript.
1. Simplicity
One of the biggest advantages of callbacks is how simple the concept is.
Callbacks are nothing more than normal JavaScript functions. There is no special syntax, no new keyword, and no additional abstraction required to understand them.
Because of this:
- You don’t need to learn any new programming paradigm.
- You can start using callbacks immediately, even as a beginner.
- The mental model is straightforward: “Call this function after something finishes.”
This simplicity made callbacks extremely popular in the early days of JavaScript, especially when asynchronous programming was still new to many developers.
2. Popularity and Wide Adoption
Callbacks are everywhere in JavaScript, especially in older codebases and widely used libraries.
Many popular APIs and frameworks were originally built using callbacks, including:
- DOM event handlers
- Node.js core APIs
- Early versions of popular libraries like Express
Because of this:
- Most JavaScript developers are already familiar with callbacks.
- You’ll often encounter callback-based code in real-world projects.
- Understanding callbacks helps you read and maintain legacy or existing applications.
In upcoming lessons, you’ll often see callbacks used in real-world examples taken from well-known JavaScript libraries.
Cons of Using Callbacks
While callbacks are simple and widely used, they also come with several serious drawbacks. These drawbacks are the main reason why modern JavaScript introduced alternatives like Promises and async/await.
1. Lack of Readability
One major drawback of callbacks is that code readability can suffer.
When callbacks are nested or chained:
- It becomes difficult to visually follow the execution flow.
- The order in which operations occur is not always obvious.
- Understanding what happens first, second, or last becomes confusing.
This problem becomes especially noticeable when:
- Multiple asynchronous operations depend on each other.
- Error handling is mixed into deeply nested callbacks.
- Business logic is scattered across multiple nested functions.
As a result, the code becomes harder to maintain and reason about.
2. Callback Hell
One of the most well-known problems with callbacks is something commonly called “callback hell.”
Callback hell occurs when:
- Multiple asynchronous operations depend on previous ones.
- Each operation is written as a nested callback.
- The code starts forming a deeply indented structure that resembles a pyramid.
This leads to:
- Poor readability
- Difficult debugging
- Increased chances of logical errors
- Code that is hard to refactor or extend
When developers encounter callback hell, it often signals that the code structure needs improvement or that a more modern approach (like Promises or async/await) should be used.
What Is Callback Hell?
Callback hell occurs when multiple asynchronous operations are nested inside each other using callbacks, creating deeply indented and difficult-to-read code.
It usually looks like a pyramid or staircase shape, which is why it is sometimes called:
“The Pyramid of Doom”
As the number of dependent callbacks increases:
- Code becomes harder to read
- Debugging becomes painful
- Logic becomes difficult to follow
- Maintenance becomes risky
Visualizing Callback Hell
Here’s what the structure looks like conceptually:
calculateSquare(1, () => {
calculateSquare(2, () => {
calculateSquare(3, () => {
calculateSquare(4, () => {
calculateSquare(5, () => {
...
});
});
});
});
});
This structure:
- Is hard to scan visually
- Makes debugging painful
- Is difficult to modify or extend
