Skip to main content

Command Palette

Search for a command to run...

Callbacks in JavaScript: Why They Exist

Updated
4 min readView as Markdown

To truly understand how JavaScript works under the hood, you have to accept one fundamental, slightly mind-bending concept: In JavaScript, functions are just values. They aren't special, magical entities locked away from the rest of your code. You can assign a function to a variable, store it in an array, or—most importantly—pass it as an argument into another function.

This single feature is the beating heart of the callback function. But what exactly is a callback, and why did it become the backbone of early JavaScript architecture? Let's break it down.

What Exactly is a Callback?

At its simplest, a callback is just a function that you pass into another function, with the expectation that it will be "called back" (executed) at a later time.

Imagine you are asking a friend to go to the store to check if they have your favorite coffee. You don't just say, "Go to the store." You say, "Go to the store, and when you find out, call me."

That "call me" instruction is the callback.

Let's look at how this looks in basic, synchronous code:

// Our simple callback function
function greetUser(name) {
  console.log(`Hello there, ${name}!`);
}

// A function that accepts a callback as an argument
function processUserInput(callback) {
  const name = "Alex";
  // Executing the callback function we passed in
  callback(name); 
}

// We pass the function itself, without the parentheses ()
processUserInput(greetUser); 
// Output: Hello there, Alex!

Notice how we passed greetUser without the parentheses? If we added (), the function would execute immediately. By leaving them off, we hand the function over as a value, saying, "Here is the code. Run it when you are ready."

The Real Power: Asynchronous Programming

While synchronous callbacks (like the one above, or using .map() on an array) are useful for organizing code, callbacks truly shine when dealing with asynchronous operations.

JavaScript is a single-threaded language. This means it has only one worker, doing one task at a time. If that worker stops to wait for a massive file to download, your entire application freezes. The buttons won't click, the animations will stop, and the user will get frustrated.

Callbacks solve this blocking problem. They allow JavaScript to say: "Start downloading this file in the background. I am going to keep running the rest of the application. When the download finishes, run this callback function."

Common Asynchronous Scenarios

If you build backend services with Node.js or handle API data on the frontend, you use asynchronous callbacks constantly. Here are a few places they show up:

1. Reading the File System (Node.js) When you ask the server to read a file, it takes time. Instead of halting the server, Node.js uses a callback to process the data once it is finally retrieved.

const fs = require('fs');

console.log("1. Requesting file...");

// The second argument is an anonymous callback function
fs.readFile('database-config.json', 'utf8', (error, data) => {
  if (error) throw error;
  console.log("3. File loaded!", data);
});

console.log("2. Doing other things while we wait...");

Output order: 1, 2, 3. The server kept working while the file was being read!

2. Browser Event Listeners Every time you attach an event listener to a button, you are using a callback. You are telling the browser, "Wait until the user clicks this, and then run my logic."

const button = document.getElementById('submit-btn');

button.addEventListener('click', function() {
  console.log("Button was clicked!");
});

The Dark Side: The Pyramid of Doom

For years, callbacks were the absolute standard for handling async JavaScript. However, as web applications grew more complex, a major architectural flaw emerged: Callback Nesting.

What happens if you need to do a sequence of asynchronous tasks, where each task depends on the previous one?

For example, imagine you need to:

  1. Find a user in the database.

  2. Use their ID to find their profile.

  3. Use their profile to find their permissions.

  4. Use their permissions to load a dashboard.

Using purely callbacks, your code starts to indent deeply to the right, forming a sideways triangle.

getUser('alex123', function(user) {
  getProfile(user.id, function(profile) {
    getPermissions(profile.role, function(permissions) {
      loadDashboard(permissions, function(dashboard) {
        console.log("Finally, the dashboard is loaded!");
      });
    });
  });
});

This is famously known as Callback Hell or the Pyramid of Doom.

It makes code incredibly difficult to read, maintain, and debug. Tracking variable scope becomes a nightmare, and handling errors at each step requires writing repetitive if (error) checks over and over again.