Callbacks in JavaScript: Why Do They Even Exist

Hello readers,
In my previous blog, we explored how JavaScript executes code—Sync vs. Async—and why things don't always run in the order we expect.
If you haven’t read it yet, I highly recommend checking that out first. Here is the link:
https://js-with-abhishek.hashnode.dev/sync-async
Because today’s topic is built on top of that understanding…
Here are some questions for you...
How does JavaScript handle tasks like API calls or timers?
How can one function “wait” for another without blocking everything?
And why do we pass functions inside functions? 🤯
That’s where callbacks enter the scene, and this blog is all about callbacks.
Before jumping into callbacks... let's understand this first
Functions are just values in JavaScript
In JavaScript, functions are first-class citizens.
That means:
You can store them in variables
Pass them as arguments
Return them from other functions
Example:
function greet() {
console.log("Hello!");
}
function execute(fn) {
fn(); // calling the function passed as argument
}
execute(greet);
Here, greet is passed as a value.
What is a Callback Function?
In a simple way, the callback function is:
A function that is passed as an argument to another function and is executed later
Example:
function processUser(name, callback) {
console.log("Processing user:", name);
callback();
}
function sayWelcome() {
console.log("Welcome!");
}
processUser("Abhishek", sayWelcome);
Here:
sayWelcomeis the callbackIt runs after
processUserdoes its job
Why do callbacks exist?
Now comes the real question — why do we even need them?
Problem without callbacks
JavaScript is single-threaded. It can do only ONE thing at a time
So what happens with slow tasks?
API calls
File reading
Timers
If JS waits for them → everything freezes
Solution: Asynchronous programming
Instead of blocking, JavaScript says:
“Hey, I’ll continue my work… you call me back when you're done.”
And that “call me back” is exactly what a callback is.
Example
console.log("Start");
setTimeout(() => {
console.log("Inside timeout");
}, 2000);
console.log("End");
Output:
Start
End
Inside timeout
The function inside the setTimeout is a callback.
Passing Functions as Arguments
Callbacks work because functions can be passed around like data.
Example:
function calculate(a, b, operation) {
return operation(a, b);
}
function add(x, y) {
return x + y;
}
console.log(calculate(2, 3, add)); // 5
addis passed as a callbackcalculatedecides when/how to execute it
Use case of callbacks
1. Timers
setTimeout(() => {
console.log("Executed after delay");
}, 1000);
2. API Calls
fetch("https://api.example.com/data")
.then((response) => response.json())
.then((data) => console.log(data));
.then() takes a callback. Don't worry, this code snippet will be covered in the next blog.
3. Event Handling
button.addEventListener("click", () => {
console.log("Button clicked!");
});
The function runs only when an event happens.
Problems with callbacks
Up until now, callbacks look clean and powerful…
But in real-world apps, things don’t stay this simple
Let’s understand this with a relatable example
To understand the problems with callbacks, imagine this scenario
Imagine you're building a system like Zomato/Swiggy
Steps involved:
Place Order
Process Payment
Assign Delivery Partner
Deliver Order
Important: Each step depends on the previous one
No payment → no delivery
No order → nothing happens
Let’s implement this using callbacksLet’s implement this using callbacks
function placeOrder(callback) {
setTimeout(() => {
console.log("Order placed");
callback();
}, 1000);
}
function processPayment(callback) {
setTimeout(() => {
console.log("Payment successful");
callback();
}, 1000);
}
function assignDelivery(callback) {
setTimeout(() => {
console.log("Delivery partner assigned");
callback();
}, 1000);
}
function deliverOrder() {
setTimeout(() => {
console.log("Order delivered");
}, 1000);
}
// Flow
placeOrder(() => {
processPayment(() => {
assignDelivery(() => {
deliverOrder();
});
});
});
Problem number 1: Pyramid of Doom
Look at that structure carefully…
placeOrder(() => {
processPayment(() => {
assignDelivery(() => {
deliverOrder();
});
});
});
This shape is called Pyramid of Doom (aka Callback Hell)
As steps increase → nesting increases
Imagine adding:
Order tracking
Notifications
Refund logic
Your code becomes a right-slanting triangle
Hard to read
Hard to maintain
Easy to break
Problem 2: Function Dependency
Each function depends on the previous one:
processPaymentdepends onplaceOrderassignDeliverydepends onprocessPaymentdeliverOrderdepends onassignDelivery
This creates tight coupling
Why is this bad?
You can’t reuse functions independently
You must follow the strict order
Changing one step may break the entire chain
Problem 3: Inversion of Control
This is a subtle but very important issue.
When you pass a callback:
placeOrder(() => {
processPayment(() => {
...
});
});
You are giving control of your function to another function
Now you’re trusting:
Will it call your callback?
Will it call it once or multiple times?
Will it call it at the right time?
You lose control over execution
Problem 4: Error Handling Nightmare
Now imagine something fails…
placeOrder(() => {
processPayment((err) => {
if (err) {
console.log("Payment failed");
} else {
assignDelivery(() => {
deliverOrder();
});
}
});
});
Error handling becomes:
Scattered
Repetitive
Ugly
And worse… You must handle errors at every level
Callbacks were powerful… but:
They don’t scale well
They make code messy
They reduce readability
And That’s Why Promises Exist
To solve:
Callback hell
Dependency issues
Error handling mess
Inversion of control
JavaScript introduced Promises
That's it for this blog. I hope you understood callbacks and the problems associated with them. In the next blog, I will discuss what promises are and how they solve these callback issues.
Until then...
Stay Consistent and keep grinding
Peace. ✌️





