Skip to main content

Command Palette

Search for a command to run...

Callbacks in Javascript the Foundation of Async of Programming.

Before Promises, before async/await there were callbacks. Let's understand why, and what problem they were solving.

Updated
8 min read
Callbacks in Javascript the Foundation of Async of Programming.
M

Hello, I’m Asim, a passionate Full Stack Developer with 1 year of hands-on experience in building scalable and user-friendly web applications. I work across both frontend and backend, enjoy solving real-world problems, and focus on writing clean, efficient, and maintainable code. I’m always eager to learn new technologies and grow as a developer.

Let's start with a very human example.

You call a pizza place and order a pizza. Now you have two choices:

  • Option A: Stay on the phone, not move, not breathe, just wait until the pizza arrives.

  • Option B: Hang up, go watch TV, and when the doorbell rings then go get the pizza.

Option B makes way more sense, right?

That "go do something when it's ready" idea that's exactly what a callback function is in JavaScript.

JavaScript runs in a browser. The browser is doing a hundred things at once handling clicks, loading images, running your code. It cannot afford to freeze and wait. So JavaScript needed a way to say: "Go do this task, and when you're done call this function."

That function you pass along? That's the callback.

In this blog, we'll cover:

  • What a callback function actually is.

  • Why JavaScript needed callbacks in the first place.

  • How to pass functions as arguments.

  • Real world callback scenarios.

  • And the classic problem that callbacks eventually created.

Let's go.


Functions Are Values in JavaScript .

Before we talk about callbacks, we need to understand one special thing about JavaScript functions are just values**.** You can store them in variables, pass them around, return them from other functions.

// Normal value stored in a variable
const name = "Ali";

// Function stored in a variable — totally valid!
const greet = function() {
  console.log("Hello!");
};

greet(); // Hello!

Just like you can pass a number or a string into a function you can pass a function into another function too**.**

function sayHello() {
  console.log("Hello!");
}

function runThis(fn) {
  fn(); // calling whatever function was passed in
}

runThis(sayHello); // Hello!

See what happened? We passed sayHello into runThis and runThis called it. That passed function is a callback**.**

Simple definition: A callback is just a function that you pass into another function, to be called later.


Why Do Callbacks Exist The Async Problem.

JavaScript is single threaded**.** It can only do one thing at a time.

Now imagine this you need to fetch some user data from a server. That takes 3 seconds. If JavaScript just sat there waiting for 3 seconds, your entire page would freeze. No clicks, no animations, nothing.

// Imagine this takes 3 seconds...
const data = fetchDataFromServer();

// JavaScript would be completely frozen here for 3 seconds
console.log(data);

This is terrible for users. Nobody wants a frozen page.

So JavaScript uses an approach called asynchronous programming instead of waiting, it says: "Start this task, keep doing other things, and when the task finishes run this callback function."

// Start the task, pass a callback for when it's done
fetchDataFromServer(function(data) {
  console.log(data); // this runs when data is ready — not before
});

// This runs immediately — page doesn't freeze!
console.log("Page is still working fine...");

Output:

Page is still working fine...
// ... 3 seconds later ...
{ name: "Ali", age: 25 }

The page kept working. The callback ran only when the data was ready. That's the whole point.


Passing Functions as Arguments Simple Examples .

Let's build up from simple to complex.

Example 1 Basic callback:

function greetUser(name, callback) {
  console.log("Hi " + name + "!");
  callback(); // call the function that was passed in
}

function sayBye() {
  console.log("Goodbye!");
}

greetUser("Sara", sayBye);
// Hi Sara!
// Goodbye!

Example 2 Inline callback (anonymous function):

You don't have to define the function separately. You can write it directly inline:

greetUser("Sara", function() {
  console.log("Goodbye!");
});
// Hi Sara!
// Goodbye!

Same result just written in one go. This style is very common in real JavaScript code.

Example 3 Callback with data:

The outer function can pass data into the callback:

function calculate(a, b, callback) {
  const result = a + b;
  callback(result); // passing the result into the callback
}

calculate(5, 3, function(answer) {
  console.log("The answer is: " + answer);
});
// The answer is: 8

The callback received the result and used it. This pattern is everywhere in JavaScript.


Callback Usage in Common Scenarios.

Callbacks aren't just a theoretical concept you've probably already used them without realizing it.


Scenario 1: setTimeout (do something after a delay):

console.log("Order placed!");

setTimeout(function() {
  console.log("Pizza delivered!"); // runs after 3 seconds
}, 3000);

console.log("Watching TV...");

// Output:
// Order placed!
// Watching TV...
// Pizza delivered!  ← after 3 seconds

setTimeout takes a callback the function to run after the delay. JavaScript doesn't wait. It keeps going.


Scenario 2: Event Listeners (do something on click):

const button = document.getElementById("myButton");

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

The second argument to addEventListener is a callback. You're saying: "When this button is clicked run this function." JavaScript doesn't know when the user will click so it waits, and calls your function when it happens.


Scenario 3: Array Methods (forEach, filter, map):

const numbers = [1, 2, 3, 4, 5];

// forEach — callback runs for every item
numbers.forEach(function(num) {
  console.log(num * 2);
});
// 2, 4, 6, 8, 10

// filter — callback decides what to keep
const evens = numbers.filter(function(num) {
  return num % 2 === 0;
});
// [2, 4]

Every single one of these takes a callback. You tell the method what to do it handles when and how to do it.


The Basic Problem Callback Nesting.

Callbacks are great until you need to do multiple async things one after another**.**

Imagine this real scenario:

  1. First, get the user ID from the server.

  2. Then, using that ID get the user's orders.

  3. Then, using those orders get the order details.

  4. Then, show the details to the user.

With callbacks, this looks like:

getUser(function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      displayDetails(details, function() {
        console.log("Done!");
        // and it could keep going...
      });
    });
  });
});

See the shape of that code? It keeps going right and inward**.** Each new task pushes the code further and further to the right.

This is called Callback Hell or sometimes the Pyramid of Doom because of its shape.

getUser(function() {
    getOrders(function() {
        getOrderDetails(function() {
            displayDetails(function() {
                // deeper...
                    // and deeper...
                        // and deeper...
            });
        });
    });
});

Why is Callback Hell a problem?

  • Hard to read you have to trace deeply nested code to understand the flow.

  • Hard to debug errors get buried inside multiple layers.

  • Hard to maintain adding one more step means another level of nesting.

  • Error handling becomes a mess you have to handle errors at every single level separately.

getUser(function(err, user) {
  if (err) { console.log("Error!"); return; }  // handle error here

  getOrders(user.id, function(err, orders) {
    if (err) { console.log("Error!"); return; }  // handle error again

    getOrderDetails(orders[0].id, function(err, details) {
      if (err) { console.log("Error!"); return; }  // and again...
    });
  });
});

Every level needs its own error handling. It gets messy very fast.

This problem is exactly why JavaScript later introduced Promises and then async/await cleaner ways to handle async code without the nesting nightmare. But to understand those, you first had to understand callbacks. And now you do.


Conclusion.

Let's take a step back and appreciate how far we've come in just one blog post.

You started with not knowing what a callback is and now you understand:

  • That functions in JavaScript are values they can be passed around just like numbers or strings.

  • That callbacks exist because JavaScript is single-threaded it needed a way to handle tasks without freezing.

  • That you've already been using callbacks in setTimeout, event listeners, and array methods.

  • And that Callback Hell is a real problem one that the JavaScript world eventually solved with better tools.

Callbacks are the foundation. Every modern async pattern in JavaScript Promises, async/await was built to fix the problems that callbacks created. So understanding callbacks isn't just history it's the context that makes everything else make sense.

One last thing Next time you write a setTimeout or a forEach you'll know exactly what's happening under the hood. That feeling of "oh, I actually get this now" that's what we're here for.

See you in the next one happy coding.

1 views