Skip to main content

Command Palette

Search for a command to run...

Callbacks in JavaScript: Why They Exist

Published
β€’8 min read
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 err at every single level

  • Logic 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, forEach

  • When 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 callbacks

  • Express.js route handlers β€” callbacks

  • Array.prototype.sort() β€” takes a comparator callback

  • setTimeout / setInterval β€” callbacks

  • Node.js fs, http modules β€” 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

More from this blog