Callbacks in JavaScript

Let's start with a real life situation, you've definitely lived through :
You call a restaurant to order food. The person on the phone says, "We'll call you back when your order is ready." So you hang up. You don't stand frozen at the phone, staring at it, waiting. You go do other things like watch TV, finish some work, make some tea. Then your phone rings, the food is ready, and then you act.
That "call us back when it's done" instruction? That's a callback.
In JavaScript, a callback is a function you hand to another function and say: "Run this when you're done."
At first glance, callbacks may seem confusing, but they are actually one of the most fundamental and powerful concepts in JavaScript.
JavaScript treats functions as “first-class citizens,” meaning they can be assigned to variables, passed as arguments, and returned from other functions. This flexibility is what makes callbacks possible.
1. What is a Callback Function?
A callback function is simply a function that is passed into another function as an argument and is executed later.
In JavaScript, functions are treated like any other value. This means you can:
Store a function in a variable
Pass a function as an argument to another function
Return a function from a function
Let's understand these one by one :
Store a Function in a Variable :
const sayHello = function() {
console.log("Hello!");
};
sayHello(); // "Hello!"
The function lives in sayHello like a value.
Passing Functions as Arguments :
// Main Function
function greet(callback) {
console.log("Getting ready to greet...");
callback(); // calling the function that was passed in
}
// Another Function
function sayHello() {
console.log("Hello!");
}
// Calling Main Function with Another function as argument
greet(sayHello);
// "Getting ready to greet..."
// "Hello!"
We passed sayHello, the function itself, not its result, into greet. Then inside greet, we called it using callback().
That passed-in function : sayHello , is the callback.
A callback is simply: a function passed into another function, to be called at a specific moment.
What a Callback Function Is
Definition and Core Concept : A callback is executed after another function finishes its task.
How Callbacks Work Internally : Think of callbacks as instructions passed to a function to run later.
2. Why Callbacks Are Used in Asynchronous Programming
JavaScript runs in a single thread. That means it does one thing at a time, one line at a time, top to bottom. Usually, that's fine. But some operations take time, like: fetching data from a server, reading a file, waiting for a timer. If JavaScript just stopped and waited for every slow operation, your entire webpage would freeze.
Imagine clicking a button on a website and having everything, scrolling, animations, all other buttons lock up for three seconds while data loads. That's what synchronous waiting would feel like. Unusable.
Callbacks help avoid this by allowing tasks to run in the background.
3. Passing Functions as Arguments
Now that we understand why callbacks exist, let's explore how to write and pass them.
There are a few different styles you'll see in real codebases, and they all mean the same thing.
Style 1 : Named function, passed by reference
function handleClick() {
console.log("Button was clicked!");
}
document.getElementById("btn").addEventListener("click", handleClick);
handleClick is defined separately and passed in by name. Notice: no () after handleClick, you're passing the function itself, not calling it.
Style 2 : Anonymous function, defined inline
document.getElementById("btn").addEventListener("click", function() {
console.log("Button was clicked!");
});
Same result. The function has no name, it's defined right there, on the spot, inside the argument. This is extremely common and perfectly valid.
Style 3 : Arrow function (modern shorthand)
document.getElementById("btn").addEventListener("click", () => {
console.log("Button was clicked!");
});
Arrow functions are ES6 syntax, shorter, cleaner, and you'll see them everywhere in modern code. For callbacks, they behave the same way.
All three styles do exactly the same job. Which one you use comes down to the situation:
Use named functions when the same callback is reused in multiple places
Use anonymous functions when the callback is short and used only once
Use arrow functions when you want modern, concise syntax
4. Callback Usage in Common Scenarios
Callbacks aren't an exotic feature you only use in special situations. They're woven into everyday JavaScript. Here are the scenarios where you'll encounter them constantly.
Array Methods : .forEach(), .map(), .filter()
Every time you use a higher-order array method, you're passing a callback:
let products = [
{ name: "Notebook", price: 120, inStock: true },
{ name: "Pen", price: 30, inStock: false },
{ name: "Backpack", price: 850, inStock: true },
];
// forEach — do something with each item
products.forEach(product => {
console.log(`\({product.name}: ₹\){product.price}`);
});
// filter — get only items that match a condition
let available = products.filter(product => product.inStock);
// [{ name: "Notebook", ... }, { name: "Backpack", ... }]
// map — transform each item into something new
let names = products.map(product => product.name);
// ["Notebook", "Pen", "Backpack"]
In every case, you're handing a function to another function. The array method handles the looping, you just describe what to do with each item.
Event Handling :
Every interactive element on a webpage uses callbacks:
button.addEventListener("click", function() {
console.log("Button clicked!");
});
API Calls
Used to handle responses after data is fetched.
Timers (setTimeout, setInterval)
Every timer in JavaScript runs via a callback. The timer itself handles the waiting, you just provide what should happen when time is up.
setTimeout(function() {
console.log("Executed after 2 seconds");
}, 2000);
5. Basic Problem of Callback Nesting
Here's where the honest part of this article comes in, because callbacks aren't perfect, and it's important to know why.
Callback Hell :
When callbacks are nested inside each other repeatedly, it creates deeply indented code.
During callback each step depends on the previous one completing. In JavaScript code, when you chain several asynchronous operations where each depends on the last, you end up writing callbacks inside callbacks inside callbacks. Like this:
loginUser(username, password, function(user) {
getUserProfile(user.id, function(profile) {
getOrderHistory(profile.id, function(orders) {
getOrderDetails(orders[0].id, function(details) {
displayOrderSummary(details, function() {
console.log("Done!");
});
});
});
});
});
loginUser(
getUserProfile(
getOrderHistory(
getOrderDetails(
displaySummary(
// we've reached the pyramid's peak
)
)
)
)
)
This pyramid structure is often called callback hell.
Why It Becomes Hard to Maintain
Difficult to read
Hard to debug
Error handling becomes complex
Conclusion
Callbacks in JavaScript, becomes clear when you understand JavaScript’s asynchronous nature. They allow developers to manage tasks efficiently without blocking execution, making applications faster and more responsive.
While callbacks are powerful, they can become difficult to manage when overused. That’s why modern JavaScript often uses Promises and async/await for cleaner code. Still, callbacks remain a foundational concept that every JavaScript developer must understand.




