CodingNic

Asynchronous JavaScript

Callbacks

Asynchronous JavaScript 20 min read

Callbacks

Objectives

By the end of this chapter, you should be able to:

  • Explain what a callback is
  • Write a function that accepts a callback and calls it
  • Recognize “callback hell” and why deeply nested callbacks get hard to read

💡 Why this matters: Callbacks are the oldest way JavaScript handles “do this, then do that once it’s done.” Promises and async/await, coming next, are both built to fix a real problem with callbacks, so it helps to feel that problem firsthand first.

What a Callback Is

You’ve already used callbacks without necessarily calling them that: any function passed into another function, to be called later, is a callback. .forEach(), .map(), and .filter() from JavaScript Fundamentals all work this way.

javascript
function greet(name, callback) {
  const message = `Hello, ${name}`;
  callback(message);
}

greet("Jordan", (message) => {
  console.log(message);
});
// Hello, Jordan

greet() doesn’t know or care what the callback does with message, logging it, storing it, whatever you passed in decides that. This separation, “do the work” versus “decide what happens with the result,” is the whole idea behind a callback.

Callbacks and Async Code

Callbacks become essential once a function’s result isn’t available immediately. setTimeout() (previous lesson) is a callback-based function itself, it can’t return a value normally, since the calling code has already moved on by the time the delay finishes. The only way to hand back a result is to call a function once it’s ready.

javascript
function fetchUserDelayed(callback) {
  setTimeout(() => {
    callback({ name: "Sam", id: 1 });
  }, 1000);
}

fetchUserDelayed((user) => {
  console.log("Got user:", user.name);
});
// (one second later) Got user: Sam

fetchUserDelayed() simulates something that takes time, a real version of this might be an actual network request instead of a timer. Either way, the pattern is the same: pass a callback, and it gets called once the result is ready.

The Problem: Callback Hell

Things get messy once several async steps need to happen in order, each one depending on the last finishing first.

javascript
function step1(callback) {
  setTimeout(() => {
    console.log("Step 1 done");
    callback();
  }, 1000);
}

function step2(callback) {
  setTimeout(() => {
    console.log("Step 2 done");
    callback();
  }, 1000);
}

function step3(callback) {
  setTimeout(() => {
    console.log("Step 3 done");
    callback();
  }, 1000);
}

step1(() => {
  step2(() => {
    step3(() => {
      console.log("All steps complete");
    });
  });
});

This works, and logs each step in order, one second apart, finishing with All steps complete. But look at the shape of it: every new step nests one level deeper than the last. This pattern, informally called “callback hell” or “the pyramid of doom,” gets harder to read and harder to change with every additional step. Adding a step4 means nesting even further inside step3’s callback. Handling an error partway through (what if step2 fails?) makes it worse still.

This isn’t a sign you’re doing callbacks wrong, it’s a real, well-known limitation of the pattern itself. Promises, next lesson, solve this exact problem.

Try It

Starter code for all three exercises:

javascript
function double(n, callback) {
  callback(n * 2);
}
  1. Call double() with 5 and a callback that logs the result. Confirm it logs 10.
  2. Write a function fetchScoreDelayed(callback) that uses setTimeout() to call callback(100) after 500ms. Call it and log the result.
  3. Write two functions, stepA(callback) and stepB(callback), each using setTimeout() to log their own name and then call callback(). Nest a call to stepB inside stepA’s callback, so stepA always finishes before stepB starts.

Recap

  • A callback is a function passed into another function, to be called later, often once some work finishes.
  • Callbacks are the only way a function like setTimeout() can hand back a result, since it can’t return something that isn’t ready yet.
  • Nesting callbacks inside callbacks to run async steps in order works, but becomes hard to read and maintain as more steps are added, known as callback hell.

Next lesson: promises, a structure built specifically to fix this problem.