cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Callbacks

Learn async callbacks, error-first callback conventions, callback hell, inversion of control, and why mixing sync and async callback timing causes bugs.

By the end, you can
  • 01
    Trace async callback orderPredict which logs happen now and which happen after the host calls back.
  • 02
    Handle errors safelyUse the Node-style callback(err, result) convention without double-running success code.
  • 03
    Recognize callback risksSpot callback hell, inversion of control, and Zalgo timing bugs before they bite.

Callbacks that finish later

A callback is a function you pass to another function so it can be called at the right time. You already met the synchronous version in Function expressions & callbacks: array methods call your function while the array method is still running. This lesson moves that idea into asynchronous work: timers, events, file reads, network requests, and any job whose answer arrives after your current code has moved on.

The key question changes from “what does this function return?” to “who will call my function, with what arguments, and when?” That is a powerful pattern, but it comes with hazards: nested pyramids, error conventions, trust, and timing bugs.

Working definition

An async callback is a function you pass to an API so the API can call it later, when an operation finishes or fails.

Real-life analogyAn async callback is leaving your phone number

Dropping off a laptop does not mean you stand at the counter for three hours. You leave your number and keep living your life. The callback is that phone number: “when the work is done, call me here.”

In real life: You leave your phone number at a repair shop
In JavaScript: You pass a callback function to an async API
In real life: The shop keeps working while you go home
In JavaScript: The host API waits while your script keeps running
In real life: They call when the laptop is ready
In JavaScript: The API invokes your callback with the result

Where the analogy stops: A repair shop is a person you can negotiate with. A JavaScript API follows its documented timing and argument rules exactly, even when those rules are inconvenient.

In this lesson, every interactive run uses real JavaScript values. The replay players show recorded, instrumented examples; the playgrounds use real setTimeout callbacks and clean up on Reset.

Async callbacks: “call me when it’s done”

STEP THROUGH

Synchronous code runs now. An asynchronous callback runs later, after the current script has finished and the host has something to report. The host might be a browser timer, a click event, or Node’s file system. The important part is that the callback is not magic: it is still a function call, just made by someone else later.

Trace an async callback
Step 0 of 5Ready
Your turn: follow the blue line

Predict the log order. The callback is real JavaScript, but this player replays the recorded order so you can inspect each moment.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => {  console.log("repair shop calls back");}, 0);console.log("walk home");
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

Notice that setTimeout does not pause the script. It registers a function and returns. The current code logs the next line, then the host calls the callback in a later task. That is why the event loop lesson matters: callbacks run later only after JavaScript finishes the current turn.

Sync or async callback?
  • [1, 2].map((n) => n * 2)
  • setTimeout(() => console.log('later'), 0)
  • buttons.forEach((button) => wire(button))
  • fs.readFile('notes.txt', (err, text) => { ... })
  • items.sort((a, b) => a.rank - b.rank)
  • button.addEventListener('click', onClick)
Try it yourself
0 of 6 correct

Sort each callback by when the receiving API calls it. If the callback runs before the API returns, it is synchronous.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Error-first callbacks: the result has two seats

INTERACTIVE

In many Node-style APIs, the callback receives the error first and the success value second: callback(err, result). If the work succeeds, err is usually null. If the work fails, err is an Error object and the result may be missing or meaningless.

Real-life analogyThe shop first says whether something went wrong

A careful repair shop starts with the bad-news slot. If something went wrong, that is the first thing you hear. If not, the rest of the call can be about the successful result.

In real life: “Bad news: we could not fix it.”
In JavaScript: callback(error)
In real life: “Good news: here is your repaired laptop.”
In JavaScript: callback(null, result)
In real life: You stop planning pickup if there is bad news
In JavaScript: if (err) return handle(err);

Where the analogy stops: Real conversations are flexible. Error-first callbacks are positional: callers must check the first argument before trusting the second.

Error-first callback playground
function readProfile(id, callback) {  setTimeout(() => {    if (id === "missing") {      callback(new Error("profile not found"));      return;    }    callback(null, { name: "Ada" });  }, 0);}
Fixed handlerPop out in the code editor (opens in a new tab)JavaScript
function finish(err, data) {  if (err) {    showError(err.message);    return;  }  showProfile(data.name);}
Output branchsuccess
  • profile: Ada
Try it yourself

The first callback argument decides the branch: truthy error means stop; falsy error means use the result.

Toggle failure and the missing return. The output updates from the same branching rule your code should use.

The tiny return after handling an error matters. It exits the callback so the success branch cannot also run. Forgetting it is a common “double behavior” bug: the UI shows an error and then tries to render missing data.

Node file APIs use this shape

In Node, fs.readFile("notes.txt", (err, text) => { ... }) is a real-world error-first callback API. You do not need Node on this page; the important convention is the first argument: check err before using text.

Callback hell: the pyramid of doom

REAL TIMERS

Callback hell happens when each async step is written inside the previous step’s callback. The right edge marches across the screen, error handling repeats, and the actual story hides in punctuation.

Real-life analogyCallback hell is a nested to-do list

Imagine a to-do list where each completed task reveals the next list inside it. The work still gets done, but it is hard to see the whole plan or change one step safely.

In real life: Each task reveals the next task
In JavaScript: Each callback starts the next callback
In real life: The final task is hard to see
In JavaScript: The final success path is deeply indented
In real life: Separate tasks are easier to read
In JavaScript: Named handlers flatten the flow

Where the analogy stops: Nested callbacks are not always wrong. A two-level example can be perfectly readable. The smell is repeated nesting plus repeated error exits.

Order a pizza with callbacks
Pyramid versionPop out in the code editor (opens in a new tab)JavaScript
function orderPizza(userId, done) {  getUser(userId, (userErr, user) => {    if (userErr) return done(userErr);    getOrders(user.id, (ordersErr, orders) => {      if (ordersErr) return done(ordersErr);      getDelivery(orders[0].id, (deliveryErr, delivery) => {        if (deliveryErr) return done(deliveryErr);        done(null, delivery.eta);      });    });  });}
Real async logsuccess
  1. Press Run to order a pizza.
Try it yourself

Success path: each callback starts the next async operation. The named version keeps the same behavior but removes the pyramid shape.

A real setTimeout simulation. Reset clears pending timers and restores the success path.

Named functions do not remove callbacks; they remove the visual pyramid. Promises, in the next lessons, go further: they make “later” into a value you can return, chain, combine, and catch. Lesson 22.6 will show how to promisify this exact error-first style.

Inversion of control: you hand someone the keys

INTERACTIVE

Passing a callback means giving another function control over your continuation. You trust it to call your callback once, at the right time, with the right arguments. Most standard APIs do. Some third-party code might call it twice, call it too early, or never call it at all.

Real-life analogyInversion of control is handing over your house keys

The keys let the contractor finish work while you are away, but they also transfer control. Defensive code sets boundaries: only one completion counts, and silence eventually becomes a timeout.

In real life: You give a contractor your keys
In JavaScript: You pass a callback to a library
In real life: You expect one visit, not zero or three
In JavaScript: You expect exactly one callback call
In real life: You set boundaries and a deadline
In JavaScript: Use once() and a timeout guard

Where the analogy stops: Good APIs document and honor their callback contract. Guards are most useful at boundaries you do not fully control.

Protect a callback you hand away
Guard helpersPop out in the code editor (opens in a new tab)JavaScript
function once(callback) {  let called = false;  return (...args) => {    if (called) return;    called = true;    callback(...args);  };} function withTimeout(callback, ms) {  const safeCallback = once(callback);  const timer = setTimeout(() => {    safeCallback(new Error("timed out"));  }, ms);  return (...args) => {    clearTimeout(timer);    safeCallback(...args);  };}
Callback resultguarded
  • callback: success
Try it yourself

The once guard ignores a second call, and the timeout guard turns silence into a controlled error.

This demo models an untrusted library. Reset restores normal behavior with both guards on.

A once() guard is small but powerful: the first call wins, later calls are ignored. A timeout guard turns “nothing happened” into a normal error path, so your UI is not left waiting forever.

Never mix sync and async callbacks (Zalgo)

STEP THROUGH

“Zalgo” is the nickname for an API that sometimes calls your callback before it returns and sometimes calls it later. A common cause is a cache: cache hits are available now, cache misses require async work. That inconsistent timing breaks code that expects to finish setup after calling the function.

Real-life analogyA food-delivery app always sends updates in order

When you place a food order, you expect every update after the order is placed. An app that sometimes sends an update during checkout and sometimes later makes the next step hard to predict.

In real life: Every update arrives after you place the order
In JavaScript: Every callback runs asynchronously
In real life: Some updates arrive immediately
In JavaScript: A cache hit calls back synchronously
In real life: One update order avoids surprises
In JavaScript: Always defer callbacks for this API

Where the analogy stops: Synchronous callbacks are fine when the API promises they are synchronous, like map. The bug is one API switching timing depending on data.

Catch Zalgo timing
Step 0 of 6Ready
Your turn: follow the blue line

Predict whether the callback runs before or after line 15. The fixed version always defers cache hits.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
 function getName(id, callback) {  if (cache.has(id)) {    callback(cache.get(id));      // sync on cache hit    return;  }  setTimeout(() => {    callback("Grace Hopper");  }, 0);} console.log("before");getName("ada", (name) => console.log(name));console.log("after");
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Change the visible call and the cache-hit fix

Changing a setting starts a fresh replay. The fixed version gives the same order for hit and miss.

A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The fix is not “make everything slow.” The fix is consistency. If an API is asynchronous, always call its callback asynchronously, even when the answer is cached. queueMicrotask or setTimeout can defer the cache-hit branch; the exact tool depends on the API’s contract.

Where you’ll use callbacks

Modern code often prefers promises, but callbacks are still everywhere because functions are the basic way JavaScript represents “do this later.” You will see them in event listeners, timers, array methods, older Node APIs, test helpers, streams, and third-party libraries.

Callback problems and common solutions
ProblemCallback-era techniquePromise-era preview
A result arrives laterPass done(err, value)Return a promise that fulfills with the value
Repeated nested stepsName each handler and return after errorsChain with .then(...) or use async functions later
Error path repeats everywhereUse error-first callbacks consistentlyUse .catch(...) for one downstream error path
Library might call twiceWrap with once()Promises settle once by design
Sync/async timing changesAlways defer callbacks for async APIsPromises always run reactions asynchronously
A real Node-style file readPop out in the code editor (opens in a new tab)JavaScript
import { readFile } from "node:fs"; readFile("profile.json", "utf8", (err, text) => {  if (err) return console.error(err.message);   const profile = JSON.parse(text);  console.log(profile.name);});

This page does not require Node, but the pattern is worth recognizing: start work, return now, then receive either an error or a result later.

Common misconceptions

“Callback” means “asynchronous.”

No. A callback is any function passed to be called by another function. map callbacks are synchronous; timer, event, and file callbacks are asynchronous.

“setTimeout(fn, 0) runs immediately.”

It schedules fn for later. The current script still runs to completion first.

“Throwing inside an async callback is how you report expected failure.”

Error-first APIs report failure by passing an error to the callback. A throw in a later callback does not travel back to the original caller’s try block.

“Callback hell is just ugly formatting.”

The indentation is a symptom. The deeper problem is tangled sequencing, duplicated error handling, and hard-to-reuse inner steps.

“If a result is cached, calling back synchronously is a harmless optimization.”

It changes the API’s timing contract. Make async APIs consistently async.

Practice: callbacks

5 EXERCISES
Exercise 1 · Warm-upPredict async log order

Predict the order before running the code.

Starter codePop out in the code editor (opens in a new tab)JavaScript
console.log("A");
setTimeout(() => console.log("B"), 0);
console.log("C");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeConvert a pyramid to named functions

    Take the pizza pyramid from the experiment and rewrite it as named handlers.

      Exercise 3 · PracticeWrite an error-first callback correctly

      Fill in a callback that handles the error branch before the success branch.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function finish(err, result) {
        if (err) {
          console.log("Error: " + err.message);
          return;
        }
        console.log("Result: " + result);
      }
      
      finish(null, "saved");
      finish(new Error("offline"));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeAdd a once guard

        Protect a callback so only the first call can do work.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        function once(callback) {
          let called = false;
          return function safeCallback(value) {
            if (called) return;
            called = true;
            callback(value);
          };
        }
        
        const logOnce = once((value) => console.log(value));
        logOnce("first");
        logOnce("second");

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFix a Zalgo function

          Change the cache-hit branch so it calls the callback later, not now.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const cache = new Map([["ada", "Ada"]]);
          function getName(id, callback) {
            if (cache.has(id)) {
              setTimeout(() => callback(cache.get(id)), 0);
              return;
            }
            setTimeout(() => callback("missing"), 0);
          }
          console.log("before");
          getName("ada", (name) => console.log(name));
          console.log("after");

          Answer, then press Check. Spacing and letter case don’t matter.

            Quiz: check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What makes a callback asynchronous?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              console.log("A");
              setTimeout(() => console.log("B"), 0);
              console.log("C");

              Choose an answer to see the explanation.

            3. Question 3 of 7In callback(err, result), what should err be when the operation succeeds?

              Choose an answer to see the explanation.

            4. Question 4 of 7Why do examples often write if (err) return done(err);?

              Choose an answer to see the explanation.

            5. Question 5 of 7Which risk is inversion of control?

              Choose an answer to see the explanation.

            6. Question 6 of 7What is wrong with a function that sometimes calls its callback now and sometimes later?

              Choose an answer to see the explanation.

            7. Question 7 of 7Which lesson fixes many callback problems by representing a future value?

              Choose an answer to see the explanation.

            Key takeaways

            • A callback is a function passed to another function; async callbacks are called later when work finishes.
            • Error-first callbacks use callback(err, result); always check and return from the error branch before using the result.
            • Callback hell is nested sequencing plus repeated error handling; named handlers flatten it, and promises improve it further.
            • Inversion of control means another API controls if and when your callback runs; guards like once() and timeouts protect boundaries.
            • Never let one async API sometimes call back synchronously and sometimes asynchronously. That timing bug is Zalgo.

            Remember the one-liner.
            A callback is a function you hand to someone else with instructions for when the work finishes.

            Up next: Promises, the fix for many callback problems.

            CompleteFrontend Clear concepts. Working examples.