Skip to main content
โณintermediate

Callbacks

Callbacks are functions passed as arguments to be called later. They're JavaScript's original async pattern โ€” understand them to understand Promises and async/await.

What is a Callback?

A callback is a function that you pass as an argument to another function, to be called at some later time:

hljs javascript
function greet(name, callback) {
  const message = `Hello, ${name}!`;
  callback(message); // call the callback with the result
}

greet("Alice", function(msg) {
  console.log(msg); // "Hello, Alice!"
});

// With arrow function
greet("Bob", (msg) => console.log(msg));

This is synchronous โ€” the callback runs immediately. But callbacks really shine in async code.

Why Callbacks Exist

JavaScript runs on a single thread. Operations like network requests, file reads, and timers can't block that thread. Callbacks let you say: "Start this operation, and when it's done, call this function."

hljs javascript
// setTimeout โ€” async callback after a delay
console.log("1: Start");

setTimeout(() => {
  console.log("3: Runs after 1 second");
}, 1000);

console.log("2: Continues immediately");

// Output order: 1, 2, 3

Callbacks in the Browser

hljs javascript
// Event listeners โ€” callback fires on user action
document.getElementById("btn").addEventListener("click", function(event) {
  console.log("Button clicked!", event.target);
});

// Array methods use callbacks (synchronous)
const nums = [1, 2, 3, 4, 5];
nums.forEach(function(n) {
  console.log(n);
});
nums.filter(n => n > 2);       // [3, 4, 5]
nums.map(n => n * 2);          // [2, 4, 6, 8, 10]

The Error-First Convention

Node.js popularized the "error-first callback" pattern. The first argument is always an error (or null if success):

hljs javascript
const fs = require("fs");

fs.readFile("data.txt", "utf8", function(err, data) {
  if (err) {
    console.error("Error reading file:", err.message);
    return; // handle error and return
  }
  console.log("File contents:", data);
});

console.log("This runs before the file is read");

Callback Hell

When async operations depend on each other, callbacks nest deeply โ€” this is called "callback hell" or the "pyramid of doom":

hljs javascript
// Simulate async operations (e.g., API calls)
function fetchUser(id, callback) {
  setTimeout(() => callback(null, { id, name: "Alice" }), 100);
}

function fetchOrders(userId, callback) {
  setTimeout(() => callback(null, [{ id: 1 }, { id: 2 }]), 100);
}

function fetchOrderDetails(orderId, callback) {
  setTimeout(() => callback(null, { id: orderId, total: 99 }), 100);
}

// The pyramid of doom:
fetchUser(1, function(err, user) {
  if (err) return handleError(err);

  fetchOrders(user.id, function(err, orders) {
    if (err) return handleError(err);

    fetchOrderDetails(orders[0].id, function(err, details) {
      if (err) return handleError(err);

      // Deeply nested โ€” hard to read and maintain
      console.log("Order details:", details);
    });
  });
});

Problems with this pattern:

  • Hard to read (indentation grows rightward)
  • Error handling must be repeated at every level
  • Hard to add branching logic
  • Difficult to test

Mitigating Callback Hell

Strategy 1 โ€” Named functions:

hljs javascript
function handleDetails(err, details) {
  if (err) return handleError(err);
  console.log("Details:", details);
}

function handleOrders(err, orders) {
  if (err) return handleError(err);
  fetchOrderDetails(orders[0].id, handleDetails);
}

function handleUser(err, user) {
  if (err) return handleError(err);
  fetchOrders(user.id, handleOrders);
}

fetchUser(1, handleUser); // much flatter

Strategy 2 โ€” Promises (the modern solution):

hljs javascript
// Same operations as Promise-based functions
fetchUser(1)
  .then(user => fetchOrders(user.id))
  .then(orders => fetchOrderDetails(orders[0].id))
  .then(details => console.log("Details:", details))
  .catch(err => handleError(err));

Promises are covered in the next lesson.

Callbacks with setTimeout and setInterval

hljs javascript
// One-time delay
const timeoutId = setTimeout(() => {
  console.log("Fires once after 2 seconds");
}, 2000);

// Cancel if needed
// clearTimeout(timeoutId);

// Repeated interval
let count = 0;
const intervalId = setInterval(() => {
  count++;
  console.log(`Tick ${count}`);
  if (count === 5) {
    clearInterval(intervalId); // stop after 5 ticks
    console.log("Done!");
  }
}, 500);

๐Ÿ’กsetTimeout(fn, 0) โ€” the async trick

setTimeout(fn, 0) doesn't run immediately โ€” it queues the callback to run "as soon as possible" after the current synchronous code finishes. It's used to defer code to the next iteration of the event loop.

hljs javascript
console.log("1");
setTimeout(() => console.log("3"), 0);
console.log("2");
// Output: 1, 2, 3

Parallel Callbacks

The old way to run multiple async operations in parallel:

hljs javascript
function parallel(tasks, callback) {
  const results = [];
  let completed = 0;

  tasks.forEach(function(task, index) {
    task(function(err, result) {
      if (err) return callback(err);
      results[index] = result;
      completed++;
      if (completed === tasks.length) {
        callback(null, results);
      }
    });
  });
}

// Modern equivalent: Promise.all()
โ–ถTry it yourself

Key Takeaways

  • Callbacks are functions passed as arguments to be called later
  • Error-first callbacks: first argument is the error, second is the result
  • Callback hell = deeply nested async callbacks โ€” hard to read and maintain
  • Named functions help flatten callback hell
  • Modern code uses Promises (then) and async/await instead โ€” but callbacks are still everywhere in event listeners, array methods, and legacy code

Ready to test your knowledge?

Take a quiz on what you just learned.

Take the Quiz โ†’