Callbacks

The original async pattern. Pass a function to be called when work is done.

Async Callbacks

A function you pass that gets called when an async operation completes.

// Synchronous callback (runs immediately):
[1, 2, 3].forEach(n => console.log(n));

// Asynchronous callback (runs LATER):
setTimeout(() => {
  console.log("I run after 1 second");
}, 1000);

The key difference: async callbacks don't run immediately. They're stored and called when the async operation finishes.

1 / 2

Error-First Pattern

Node.js convention: first argument is always the error.

// Error-first callback pattern:
function readFile(path, callback) {
  setTimeout(() => {
    if (path === "") {
      callback(new Error("Path is empty"), null);
    } else {
      callback(null, "file contents here");
    }
  }, 100);
}

// Usage — always check error first:
readFile("data.txt", (error, data) => {
  if (error) {
    console.error("Failed:", error.message);
    return; // stop here!
  }
  console.log("Got:", data);
});

Convention:

callback(error, result)

• If error is null → success, use result

• If error exists → failure, handle error

Callback Hell

Nested callbacks become unreadable. The 'Pyramid of Doom'.

// The problem: sequential async operations
getUser(userId, (err, user) => {
  if (err) return handleError(err);
  getOrders(user.id, (err, orders) => {
    if (err) return handleError(err);
    getOrderDetails(orders[0].id, (err, details) => {
      if (err) return handleError(err);
      getShipping(details.trackingId, (err, shipping) => {
        if (err) return handleError(err);
        // 😱 4 levels deep! And it gets worse...
        displayShipping(shipping);
      });
    });
  });
});

Problems with callback hell:

  • • Hard to read (grows to the right)
  • • Hard to handle errors (repeated error checks)
  • • Hard to debug (unclear execution flow)
  • • Hard to add/remove steps
  • • Variables trapped in scopes

Fixing Callback Hell

Named functions and modularization help — but Promises are the real solution.

// Fix 1: Named functions (flatten the pyramid)
function handleUser(err, user) {
  if (err) return handleError(err);
  getOrders(user.id, handleOrders);
}

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

function handleDetails(err, details) {
  if (err) return handleError(err);
  getShipping(details.trackingId, displayShipping);
}

// Start the chain:
getUser(userId, handleUser);
// Fix 2: Promises (the modern solution)
getUser(userId)
  .then(user => getOrders(user.id))
  .then(orders => getOrderDetails(orders[0].id))
  .then(details => getShipping(details.trackingId))
  .then(shipping => displayShipping(shipping))
  .catch(handleError); // one error handler!

// Fix 3: Async/Await (cleanest)
async function showShipping(userId) {
  const user = await getUser(userId);
  const orders = await getOrders(user.id);
  const details = await getOrderDetails(orders[0].id);
  const shipping = await getShipping(details.trackingId);
  displayShipping(shipping);
}

Real Examples

Where you still see callbacks today.

// Event listeners (always callbacks):
button.addEventListener("click", () => {
  console.log("Clicked!");
});

// Array methods:
[1, 2, 3].map(n => n * 2);

// setTimeout / setInterval:
setTimeout(() => console.log("later"), 1000);

// Node.js fs (legacy API):
const fs = require("fs");
fs.readFile("file.txt", "utf8", (err, data) => {
  if (err) throw err;
  console.log(data);
});

// Geolocation API:
navigator.geolocation.getCurrentPosition(
  (pos) => console.log(pos.coords),
  (err) => console.error(err)
);

Callbacks aren't dead — event listeners, array methods, and many APIs still use them. But for sequential async operations, Promises and async/await are far better.

FAQ

Common questions about callbacks.