Skip to main content

Command Palette

Search for a command to run...

Callbacks in JavaScript: Why They Exist

Callbacks Explained: The Art of Passing Functions Like Notes in Class πŸ“

Updated
β€’10 min read
J

Turning chai into code and ideas into full-stack applications. Sharing lessons from my development journey, one commit at a time.

The Coffee Shop Revelation β˜•

Last semester, I worked part-time at a campus coffee shop. One day, my manager gave me a task:

"Make a latte. When you're done, call out the customer's name so they can pick it up."

Simple, right? But think about what just happened:

  1. I was given a task (make latte)

  2. I was given another task to do AFTER (call the name)

  3. The second task depends on the first being complete

This is exactly how callback functions work in JavaScript.

I didn't realize it then, but I was living a callback-driven life. The customer order was the main function. Calling the name was the callback. My brain was JavaScript, and I was about to have an epiphany.


What IS a Callback Function? πŸ€”

A callback is simply a function passed as an argument to another function, to be executed later.

Think of it like this:

// Regular function call
function greet() {
  console.log("Hello!");
}

greet(); // Execute immediately

// Callback function
function greet(callback) {
  console.log("Hello!");
  callback(); // Execute the passed function
}

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

greet(sayGoodbye);
// Output:
// Hello!
// Goodbye!

Why Functions Can Be Passed Around

In JavaScript, functions are first-class citizens. This means:

  • Functions can be stored in variables

  • Functions can be passed as arguments

  • Functions can be returned from other functions

  • Functions can be stored in data structures

// Functions are values!
const add = function(a, b) {
  return a + b;
};

const subtract = function(a, b) {
  return a - b;
};

// We can store functions in an array
const operations = [add, subtract];

// We can call them later
console.log(operations[0](5, 3)); // Output: 8
console.log(operations[1](5, 3)); // Output: 2

The "Why" Behind Callbacks: Asynchronous JavaScript 🌊

The Restaurant Analogy

Imagine you're at a restaurant:

// SYNCHRONOUS (Blocking) - Everyone waits
function badRestaurant() {
  takeOrder();        // Waiter takes order (1 min)
  waitForFood();      // Kitchen cooks (30 min) 🚫 WAITER JUST STANDS THERE
  deliverFood();      // Waiter delivers (1 min)
  // Total: 32 minutes per customer. Restaurant goes bankrupt.
}

// ASYNCHRONOUS (Non-blocking) - Use callbacks!
function goodRestaurant() {
  takeOrder(function whenFoodIsReady() {
    deliverFood();
  });
  // Waiter immediately takes other orders while kitchen cooks
  // Total: 32 minutes cooking time, but waiter serves 10 tables
}

Real JavaScript Example

// This DOESN'T exist in JavaScript (blocking sleep)
function wait(seconds) {
  // Pause execution for X seconds
  // JavaScript doesn't work like this!
}

console.log("Start");
wait(2); // Can't do this
console.log("End");

// This is how JavaScript handles delays
console.log("Start");

setTimeout(function callback() {
  console.log("After 2 seconds");
}, 2000);

console.log("End");

// Output:
// Start
// End
// After 2 seconds (appears 2 seconds later)

Key Insight: JavaScript doesn't wait. It uses callbacks to say "run this when you're ready."


Callback Patterns: From Simple to Complex 🎯

Pattern 1: Simple Callbacks (Synchronous)

These execute immediately:

// Array methods use callbacks!
const numbers = [1, 2, 3, 4, 5];

numbers.forEach(function(number) {
  console.log(number * 2);
});
// Output: 2, 4, 6, 8, 10

// With arrow functions (modern syntax)
numbers.forEach(number => console.log(number * 2));

Real-world example:

const students = [
  { name: "Alice", score: 85 },
  { name: "Bob", score: 92 },
  { name: "Charlie", score: 78 }
];

students.forEach(function(student) {
  if (student.score >= 90) {
    console.log(`${student.name} gets an A!`);
  }
});
// Output: Bob gets an A!

Pattern 2: Asynchronous Callbacks

These execute later, when something finishes:

// Simulating an API call
function fetchUserData(userId, callback) {
  console.log(`Fetching user ${userId}...`);
  
  // Simulate network delay
  setTimeout(function() {
    const user = { id: userId, name: "Alice", email: "alice@example.com" };
    callback(user); // Execute callback when data arrives
  }, 2000);
}

// Usage
console.log("Start");

fetchUserData(123, function(userData) {
  console.log("Got user:", userData);
});

console.log("End");

// Output:
// Start
// Fetching user 123...
// End
// (2 seconds later)
// Got user: { id: 123, name: 'Alice', email: 'alice@example.com' }

Pattern 3: Error-First Callbacks (Node.js Convention)

The standard way to handle errors in callbacks:

function readFile(filename, callback) {
  setTimeout(function() {
    // Simulate random error
    const error = Math.random() > 0.5 ? null : new Error("File not found");
    const data = error ? null : "File contents here";
    
    callback(error, data); // First arg: error, Second arg: data
  }, 1000);
}

// Usage
readFile("data.txt", function(error, data) {
  if (error) {
    console.log("Error:", error.message);
    return;
  }
  
  console.log("Success:", data);
});

Why error-first? Consistency! Every callback follows the same pattern:

  1. Check if error exists

  2. Handle error if needed

  3. Process data if no error


Real-World Callback Examples 🌍

Example 1: Button Click Event

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

// addEventListener takes a callback
button.addEventListener("click", function() {
  console.log("Button was clicked!");
  // This runs WHEN user clicks, not immediately
});

console.log("Event listener added");
// Output immediately: "Event listener added"
// Output on click: "Button was clicked!"

Example 2: Fetching Data (Old Style)

function getWeather(city, callback) {
  console.log(`Fetching weather for ${city}...`);
  
  // Simulate API call
  setTimeout(function() {
    const weather = {
      city: city,
      temp: 72,
      condition: "Sunny"
    };
    callback(weather);
  }, 1500);
}

getWeather("New York", function(weatherData) {
  console.log(`It's \({weatherData.temp}Β°F and \){weatherData.condition} in ${weatherData.city}`);
});

// Output:
// Fetching weather for New York...
// (1.5 seconds later)
// It's 72Β°F and Sunny in New York

Example 3: Custom Timer

function countdown(seconds, onTick, onComplete) {
  let remaining = seconds;
  
  const interval = setInterval(function() {
    onTick(remaining); // Callback for each second
    remaining--;
    
    if (remaining < 0) {
      clearInterval(interval);
      onComplete(); // Callback when done
    }
  }, 1000);
}

// Usage
countdown(
  3,
  function onTick(seconds) {
    console.log(`${seconds} seconds remaining...`);
  },
  function onComplete() {
    console.log("Time's up!");
  }
);

// Output:
// 3 seconds remaining...
// 2 seconds remaining...
// 1 seconds remaining...
// 0 seconds remaining...
// Time's up!

The Dark Side: Callback Hell πŸ”₯

Remember the coffee shop? Imagine this conversation:

Manager: "Make a latte, then when done, call the name, then when they pick it up, clean the counter, then when clean, take the next order, then..."

This is callback hell (also called Pyramid of Doom):

// Callback Hell - HARD TO READ
getUserData(function(user) {
  getOrders(user.id, function(orders) {
    getOrderDetails(orders[0].id, function(details) {
      getProductInfo(details.productId, function(product) {
        getReviews(product.id, function(reviews) {
          console.log("Finally got reviews:", reviews);
        });
      });
    });
  });
});

Problems:

  1. Readability: Code grows horizontally (pyramid shape)

  2. Error handling: Need to handle errors at EVERY level

  3. Maintenance: Adding/removing steps is painful

  4. Debugging: Stack traces become nightmares

A Real Horror Story:

// My first project (I'm not proud of this)
function processUserRegistration(email, password) {
  validateEmail(email, function(isValid) {
    if (isValid) {
      checkEmailExists(email, function(exists) {
        if (!exists) {
          hashPassword(password, function(hash) {
            createUser({ email, password: hash }, function(user) {
              sendWelcomeEmail(user.email, function(sent) {
                if (sent) {
                  logActivity("user_registered", user.id, function() {
                    redirectToHome(function() {
                      console.log("Registration complete!");
                    });
                  });
                }
              });
            });
          });
        }
      });
    }
  });
}

// Seven levels of nesting. SEVEN. 😱

Solving Callback Hell: Better Patterns πŸ› οΈ

Solution 1: Named Functions

// Extract callbacks into named functions
function handleReviews(reviews) {
  console.log("Got reviews:", reviews);
}

function handleProduct(product) {
  getReviews(product.id, handleReviews);
}

function handleOrderDetails(details) {
  getProductInfo(details.productId, handleProduct);
}

function handleOrders(orders) {
  getOrderDetails(orders[0].id, handleOrderDetails);
}

function handleUser(user) {
  getOrders(user.id, handleOrders);
}

// Usage
getUserData(handleUser);

Benefits: Each function has a clear purpose, easier to test and debug.


Solution 2: Modern Alternatives (Preview)

// Promises (we'll cover in another post)
getUserData()
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(details => getProductInfo(details.productId))
  .then(product => getReviews(product.id))
  .then(reviews => console.log("Got reviews:", reviews));

// Async/await (even better!)
async function getReviewsForUser() {
  const user = await getUserData();
  const orders = await getOrders(user.id);
  const details = await getOrderDetails(orders[0].id);
  const product = await getProductInfo(details.productId);
  const reviews = await getReviews(product.id);
  console.log("Got reviews:", reviews);
}

Callback Best Practices πŸ’‘

1. Keep Callbacks Small

// Too much logic in callback
button.addEventListener("click", function() {
  const input = document.getElementById("input").value;
  if (input.length > 0) {
    const processed = input.trim().toLowerCase();
    const valid = /^[a-z]+$/.test(processed);
    if (valid) {
      fetch(`/api/search?q=${processed}`)
        .then(res => res.json())
        .then(data => {
          // ... more logic
        });
    }
  }
});

// Extract to named function
function handleSearch() {
  const input = getInputValue();
  if (validateInput(input)) {
    searchAPI(input);
  }
}

button.addEventListener("click", handleSearch);

2. Handle Errors Properly

// Ignoring errors
fetchData(function(data) {
  console.log(data.results); // What if data is null?
});

// Always check for errors
fetchData(function(error, data) {
  if (error) {
    console.error("Failed to fetch:", error);
    showErrorMessage();
    return;
  }
  
  if (data && data.results) {
    console.log(data.results);
  }
});

3. Avoid Deep Nesting

Rule of thumb: If nesting goes beyond 2-3 levels, refactor!

// Too deep
a(function() {
  b(function() {
    c(function() {
      d(function() {
        // ...
      });
    });
  });
});

// Flatten with named functions or promises

Interview Questions & Answers πŸ’Ό

Q: What is a callback function?

Good Answer: "A callback is a function passed as an argument to another function, to be executed at a later time. It's commonly used for handling asynchronous operations like API calls, timers, or event handlers. For example, when you click a button, the function you pass to addEventListener is a callback that executes when the click event occurs."


Q: What's the difference between synchronous and asynchronous callbacks?

Good Answer: "Synchronous callbacks execute immediately in sequence, like array.forEach(). Asynchronous callbacks execute later, when an operation completes, like setTimeout() or API calls. With async callbacks, JavaScript doesn't waitβ€”it continues executing other code, and the callback runs when ready."


Q: What is callback hell and how do you solve it?

Good Answer: "Callback hell occurs when you have multiple nested callbacks, creating deeply indented 'pyramid' code that's hard to read and maintain. Solutions include using named functions instead of anonymous ones, adopting Promises with .then() chaining, or using modern async/await syntax which makes asynchronous code look synchronous."


Practice Challenge πŸ‹οΈ

Build a simple traffic light system using callbacks:

function trafficLight(callback) {
  console.log("πŸ”΄ RED");
  
  setTimeout(function() {
    console.log("🟑 YELLOW");
    
    setTimeout(function() {
      console.log("🟒 GREEN");
      
      setTimeout(function() {
        callback(); // Start over or end
      }, 3000);
    }, 1000);
  }, 3000);
}

// Usage
trafficLight(function() {
  console.log("Cycle complete!");
});

Challenge: Refactor this using named functions to avoid nesting!

Solution:

function showRed(callback) {
  console.log("πŸ”΄ RED");
  setTimeout(callback, 3000);
}

function showYellow(callback) {
  console.log("🟑 YELLOW");
  setTimeout(callback, 1000);
}

function showGreen(callback) {
  console.log("🟒 GREEN");
  setTimeout(callback, 3000);
}

function complete() {
  console.log("Cycle complete!");
}

// Usage
showRed(function() {
  showYellow(function() {
    showGreen(complete);
  });
});

The Coffee Shop Ending β˜•

Back at the coffee shop, I finally understood why my manager's instructions made sense. She wasn't just telling me what to doβ€”she was implementing a callback-driven system.

The order: Main function
"Call the name when done": Callback function
Me: JavaScript runtime

Every time I made a latte and called a name, I was executing a callback. Every time I took multiple orders and managed them asynchronously, I was living the JavaScript event loop.

Programming isn't abstractβ€”it's life, systematized.


What's Next? πŸš€

Callbacks are the foundation, but they're not the end of the story. JavaScript evolved:

  1. Callbacks (1995) - We are here

  2. Promises (2015) - Better async handling

  3. Async/Await (2017) - Async code that looks sync

Each builds on the last. Master callbacks, and Promises will make perfect sense.