Callbacks in JavaScript: Why They Exist
JavaScript treats functions like values. You can store them, pass them to another function, and call them later. That flexibility is the reason callbacks exist.
This article moves from simple function arguments to asynchronous programming and callback nesting.
Functions are values
function sayHello() {
console.log("Hello!");
}
const greet = sayHello;
greet(); // Hello!
sayHello is the function itself. sayHello() runs it.
What is a callback?
A callback is a function passed into another function so that it can be run later.
function welcome(name, afterWelcome) {
console.log(`Welcome, ${name}!`);
afterWelcome();
}
function showMenu() {
console.log("Here is today's menu.");
}
welcome("Vasu", showMenu);
flowchart LR
A[Call welcome] --> B[Print greeting]
B --> C[Run callback]
C --> D[Show menu]
We pass showMenu, not showMenu(), because welcome decides when to call it.
Why callbacks are useful
Callbacks make code reusable. One function can provide data while another function decides what to do with it.
function processNumber(number, action) {
return action(number);
}
console.log(processNumber(5, (value) => value * 2)); // 10
console.log(processNumber(5, (value) => value * value)); // 25
Common callback scenarios
Arrays
const prices = [100, 250, 80];
const discounted = prices.map((price) => price * 0.9);
const expensive = prices.filter((price) => price > 100);
map and filter call your callback for each array item.
Click events
button.addEventListener("click", () => {
console.log("Button clicked");
});
The browser runs this callback only when a user clicks.
Timers
console.log("Order received");
setTimeout(() => {
console.log("Food is ready");
}, 2000);
console.log("Preparing payment");
The timeout callback runs later, so JavaScript can continue with other work first.
Callbacks and asynchronous JavaScript
Slow tasks—network calls, timers, files, database work—should not freeze the app. JavaScript starts the task, keeps running other code, and executes a callback when the task finishes.
flowchart LR
A[Start async task] --> B[Continue other work]
B --> C[Task completes]
C --> D[Callback runs]
Here is a classic Node.js-style callback:
readFile("notes.txt", "utf8", (error, content) => {
if (error) {
console.error(error);
return;
}
console.log(content);
});
Many Node.js APIs use the “error-first” callback pattern: check the error first, then use the result.
The drawback: callback nesting
When several asynchronous steps depend on each other, callbacks can become deeply nested:
getUser(userId, (user) => {
getOrders(user.id, (orders) => {
getPaymentStatus(orders[0].id, (status) => {
sendNotification(status, () => {
console.log("All done");
});
});
});
});
This is known as callback hell. The issue is readability, repeated error handling, and difficulty changing the flow—not that callbacks are inherently bad.
Modern JavaScript often uses Promises and async/await for these sequences:
async function notifyAboutFirstOrder(userId) {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const status = await getPaymentStatus(orders[0].id);
await sendNotification(status);
}
Final takeaway
Callbacks let JavaScript say: “When this work is done, run this next function.” They power array methods, browser events, timers, and asynchronous APIs.
Learn callbacks first—the ideas behind Promises and async/await become much easier afterward.
Top comments (0)