Callbacks in JavaScript: Why They Exist

Written by Saurabh Prajapati Software Engineer @ IBM India Software Lab
π₯ Hook β The Moment That Confused Me
I remember the first time I saw this in someone's code:
fs.readFile('data.txt', function(err, data) {
console.log(data);
});
I stared at it and thought β "Why are we passing a functionβ¦ inside another function?"
It felt weird. Like, why not just return the data and use it?
That confusion is exactly what this blog is about. By the end, you'll not only understand what callbacks are β you'll understand why JavaScript needed them in the first place.
π Why This Problem Matters
JavaScript runs in the browser. And the browser does a lot of waiting.
Waiting for a network request to finish
Waiting for a file to be read
Waiting for a user to click a button
If JavaScript stopped and waited for each of these β your entire page would freeze. No scrolling. No interaction. Nothing.
That's a terrible user experience.
Callbacks were JavaScript's first answer to this problem. They're the foundation of async programming in JS β and even though we have Promises and async/await today, they're all built on the same idea.
Understanding callbacks = understanding JavaScript's soul.
π§ Basic Concept β Start Here
Functions are Values in JavaScript
This is the key insight that makes callbacks possible.
In most languages, a function is just a function. You define it, you call it. That's it.
But in JavaScript, a function is a value β just like a number or a string.
Which means you can:
// Store a function in a variable
const greet = function() {
console.log("Hello!");
};
// Pass a function as an argument
function runIt(fn) {
fn(); // call whatever was passed in
}
runIt(greet); // prints: Hello!
Read that again. We passed greet into runIt β and runIt called it.
That function we passed? That's a callback.
A callback is just a function you pass to another function, so that other function can call it later.
Simple. That's it.
A Real-World Analogy
Imagine you order food at a restaurant.
You don't stand at the counter, frozen, waiting for your food. That would be ridiculous.
Instead, you give the waiter your number (the callback). You go sit down. When the food is ready, they call you back.
JavaScript works the same way.
β οΈ Where Things Break β The Async Reality
Here's where it gets interesting.
Why Can't We Just Return the Value?
Let's say you want to read a file:
// You wish this worked...
const data = readFile('data.txt');
console.log(data); // undefined
This returns undefined. Not because readFile is broken β but because the file isn't ready yet.
JavaScript didn't wait. It moved on. By the time readFile finished, your console.log had already run and left.
This is the core problem of asynchronous programming.
So We Use a Callback:
readFile('data.txt', function(err, data) {
// This runs AFTER the file is ready
console.log(data); // β
works!
});
Now we're not asking for the value immediately. We're saying:
"Hey, when you're done β call this function with the result."
That's the entire philosophy of callbacks.
π₯ The Aha Moment
Here's what clicked for me:
Callbacks don't solve async problems by waiting. They solve it by saying what to do next.
Instead of blocking the thread (like most languages do), JavaScript hands off the task and continues running. When the task is done, it calls your function.
This is what makes JavaScript non-blocking. This is why it can handle thousands of users in a web server without breaking a sweat.
It's not magic. It's just callbacks.
π οΈ Callbacks in Common Scenarios
1. Event Listeners
The most common callback you'll ever write:
document.getElementById('btn').addEventListener('click', function() {
console.log('Button clicked!');
});
You're saying: "When this button is clicked, run this function."
That function? Callback.
2. setTimeout / setInterval
console.log("Start");
setTimeout(function() {
console.log("This runs after 2 seconds");
}, 2000);
console.log("End");
Output:
Start
End
This runs after 2 seconds
Wait β End printed before the timeout? Yes!
JavaScript didn't stop at setTimeout. It registered the callback and moved on. After 2 seconds, the callback was called.
This confused me a lot at first. Now it makes total sense.
3. Array Methods
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(function(num) {
return num * 2;
});
console.log(doubled); // [2, 4, 6, 8]
map takes a callback and calls it for every element. This is a synchronous callback β but it's still a callback.
Callbacks aren't only for async code. They're for "tell me what to do" situations.
4. Node.js File Reading
const fs = require('fs');
fs.readFile('notes.txt', 'utf8', function(err, data) {
if (err) {
console.error("Something went wrong:", err);
return;
}
console.log("File contents:", data);
});
Classic Node.js pattern. The callback gets two arguments: err and data. Always check err first.
β οΈ The Basic Problem β Callback Nesting
Now here's where things get ugly.
What if you need to do multiple async operations in sequence?
getUser(userId, function(err, user) {
getPosts(user.id, function(err, posts) {
getComments(posts[0].id, function(err, comments) {
getAuthor(comments[0].authorId, function(err, author) {
console.log(author.name);
// ... and it keeps going
});
});
});
});
This is called Callback Hell (or the Pyramid of Doom).
Look at the shape of that code. It keeps going right. It's hard to read, hard to debug, and easy to mess up.
Problems with this:
Error handling becomes a nightmare β you have to handle
errat every single levelLogic is hard to follow β the flow is buried inside nesting
Debugging is painful β stack traces are confusing
Reuse is nearly impossible β the code is tightly coupled
Why Does This Happen?
Because each operation depends on the result of the previous one. You need the user before you can fetch posts. You need posts before you can fetch comments.
With callbacks, the only way to chain dependent async tasks is to nest them. And nesting = the pyramid.
This is the conceptual problem that Promises and async/await were designed to solve.
But that's a story for another blog.
βοΈ Tradeoffs
| Callbacks | |
|---|---|
| β Simple | Easy to understand for single async tasks |
| β Flexible | Works for both sync and async patterns |
| β Universal | Supported everywhere, no extra syntax |
| β Readability | Gets messy with nested operations |
| β Error handling | Must handle errors manually at every level |
| β Debugging | Hard to trace through deeply nested callbacks |
When to use callbacks:
Event listeners (always)
Simple one-off async tasks
Array methods like
map,filter,forEachWhen you're working in older codebases
When NOT to use raw callbacks:
When you have 3+ dependent async operations
When you need clean error handling across steps
When readability matters (use Promises or async/await instead)
π Real-World Usage
Callbacks are everywhere, even if you don't notice them:
React's
onClick,onChangeβ all callbacksExpress.js route handlers β callbacks
Array.prototype.sort()β takes a comparator callbacksetTimeout/setIntervalβ callbacksNode.js
fs,httpmodules β callback-based APIs
Even inside Promises and async/await, the engine is using callbacks under the hood. You're just writing cleaner syntax on top.
π§Ύ Final Summary
| Concept | What It Means |
|---|---|
| Callback | A function passed as an argument to another function |
| Why we need them | JavaScript is non-blocking β we can't just "wait" for results |
| Sync callbacks | Used in map, filter, event handling |
| Async callbacks | Used in setTimeout, file I/O, network requests |
| Callback Hell | Deeply nested callbacks from sequential async operations |
| The fix | Promises β async/await (built on same callback concept) |
Core insight: A callback is just JavaScript's way of saying β "Don't wait. When you're ready, call me."
π What's Next?
Now that you understand callbacks, the natural next step is:
Promises β how JavaScript cleaned up the callback mess
async/await β how we write async code that looks synchronous
Event Loop β the engine that makes all of this work
Each one builds on callbacks. Understanding this foundation makes everything else easier.
Written by Saurabh Prajapati (SP) Software Engineer @ IBM India Software Lab Builds scalable web & AI systems Β· Loves system design & real-world problems Β· Shares learnings openly



