completefrontend
Open lab My progress
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
    Predict when a callback runsTell which logs print now, and which print later, when the browser calls your callback.
  • 02
    Handle errors safelyUse the Node style callback(err, result), and stop the success code from running after an error.
  • 03
    Spot callback problemsRecognize callback hell, inversion of control, and Zalgo timing bugs, and know how to fix each one.

Callbacks that finish later

A callback is a function you pass to another function. The other function calls it for you, at the right time.

You met callbacks before, in Function expressions & callbacks. There, array methods like map called your function straight away, while they were still running.

This lesson is about callbacks that run later. For example: a timer that ends, a click on a button, a file that finishes loading, or an answer from a server. In each case, the answer arrives after the rest of your code has moved on.

So the question changes. You don't ask "what does this function return?" You ask three things:

  • Who will call my function?
  • What values will it pass in?
  • When will it call it?

Callbacks are very useful, but they can go wrong in a few ways. This lesson covers each one: code nested deeper and deeper, handling errors, trusting other people's code, and timing bugs.

The main idea

An async callback is a function you pass to other code, so that code can call it later, when a job finishes or fails. "Async" is short for asynchronous. It means "happens later, not right now".

A callback that runs right awayPop out in the code editor (opens in a new tab)JavaScript
function greetUser(name, done) {
  done("Hello, " + name);
}

greetUser("Asha", (message) => console.log(message));

Line 1 makes a function, greetUser. It takes a name and a function called done. Line 2 calls done straight away with a greeting. Line 5 calls greetUser and passes in a small arrow function as done.

It prints Hello, Asha before greetUser finishes. A callback that runs before the other function finishes is called synchronous, or "sync".

Real-life analogyLeaving your phone number at a repair shop

You drop off a broken laptop at a repair shop. You don't stand at the counter for three hours. You leave your phone number and go home. When the laptop is ready, the shop calls you. The callback is your phone number: "when the job is done, call me here."

In real life: You leave your phone number at a repair shop
In JavaScript: You pass a callback to a function like setTimeout
In real life: The shop works on your laptop while you go home
In JavaScript: The browser waits while the rest of your script runs
In real life: The shop calls you when the laptop is ready
In JavaScript: The browser calls your callback with the result

Every example on this page runs real JavaScript. The step-through players replay a recorded run, one step at a time. The playgrounds use real setTimeout timers, and Reset stops any timer that is still waiting.

Async callbacks: "call me when it's done"

STEP THROUGH

Sync code runs now, one line after another. An async callback runs later: after the current script has finished, when the host has something to report.

The host is the program that runs your JavaScript, like the browser or Node. It looks after timers, clicks, and files. An async callback is not magic. It is still a normal function call. The only difference is that the host makes the call, and it makes it later.

Step through a callback that runs later
Step 0 of 5Ready
Your turn: follow the blue line

Guess the order of the three logs first. This player replays a real run, one step at a time.

Running in
  1. script
Next: line 1
setTimeout(() => {  console.log("repair shop calls back");}, 0);console.log("walk home");
CallStoreChangeResultBlue line: runs next. 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.
Three cards in the order they print: drop off laptop from line 1, now. walk home from line 5, now. repair shop calls back from line 3, inside the callback, later.the order they printdrop off laptopwalk homerepair shop calls backline 1nowline 5nowline 3, in the callbacklater
Lines 1 and 5 print now. The callback on line 3 prints later, after the rest of the script is done.

setTimeout does not pause the script. It hands your function to the host and finishes at once. So the script goes on to line 5 and prints walk home. Only after the whole script is done does the host call your callback. The event loop lesson explains why callbacks wait for the current script to finish.

Send a result back laterPop out in the code editor (opens in a new tab)JavaScript
function loadGreeting(done) {
  setTimeout(() => done("Hello, Asha"), 0);
}

loadGreeting((message) => console.log(message));

Line 1 makes loadGreeting, which takes one callback, done. Line 2 starts a timer, and loadGreeting finishes straight away, without a greeting. Later the timer calls done with "Hello, Asha". The callback from line 5 then decides what to do with the greeting: here, it prints it.

A plain return can't work here. The function returns before the timer ends, so it has no greeting to give back yet.

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 it runs. If it runs before the function you passed it to finishes, it is sync. If it runs later, it is async.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
An array callback runs right awayPop out in the code editor (opens in a new tab)JavaScript
const shoppingList = ["tea", "soap"];
const food = shoppingList.filter((item) => item === "tea");
console.log(food.join(", "));

Line 2 runs filter. It calls your callback for "tea" and then for "soap", before filter finishes. Only "tea" passes the test, so line 3 prints tea. This is still a callback. It just runs now, not later.

Error-first callbacks: error first, result second

INTERACTIVE

Many Node functions call your callback with two values, in this order:

  • First, the error. If the job worked, this is null, which means "no error".
  • Second, the result. It is only there if the job worked.

So the callback looks like callback(err, result). This style is called an error-first callback. If the job fails, err is an Error object, and the result is missing or can't be trusted.

Real-life analogyThe shop tells you the bad news first

A good repair shop tells you about any problem first. If something went wrong, that is the first thing you hear. If nothing went wrong, the rest of the call is about your fixed laptop.

In real life: "Sorry, we could not fix it."
In JavaScript: callback(error)
In real life: "Good news: your laptop is fixed."
In JavaScript: callback(null, result)
In real life: If the news is bad, you don't go to pick it up
In JavaScript: if (err) return handle(err);
Try an error-first callback
A function that calls back error firstPop out in the code editor (opens in a new tab)JavaScript
function readProfile(id, callback) {  setTimeout(() => {    if (id === "missing") {      callback(new Error("profile not found"));      return;    }    callback(null, { name: "Ada" });  }, 0);}
Callback that returns after the errorPop out in the code editor (opens in a new tab)JavaScript
function finish(err, data) {  if (err) {    showError(err.message);    return;  }  showProfile(data.name);}
What gets shownworks
  • profile: Ada
Try it yourself

The first value decides the branch. If there is an error, stop. If the error is null, use the result.

Switch between It works and It fails, and try forgetting the return. The output follows the same rule your code should use.

Look at the short return inside the error branch. It stops the callback, so the success code can't run too. People often forget it. Then the page shows an error, and also tries to show data that isn't there.

Check the error before the resultPop out in the code editor (opens in a new tab)JavaScript
function saveNote(text, done) {
  if (!text) return done(new Error("empty note"));
  done(null, "saved");
}

saveNote("tea", (err, result) => {
  if (err) return console.log("error: " + err.message);
  console.log(result);
});

saveNote("", (err, result) => {
  if (err) return console.log("error: " + err.message);
  console.log(result);
});
Left: saveNote with tea calls the callback with null first and saved second, so it prints saved. Right: saveNote with an empty text calls the callback with an Error first and nothing second, so it prints error: empty note.saveNote("tea", …)null"saved"1st: err2nd: resultprintssavedsaveNote("", …)Errorundefined1st: err2nd: resultprintserror: empty note
The callback always gets the error first and the result second. When the error is null, the callback uses the result.

The first call passes "tea", so saveNote calls back with null as the error. The callback prints saved.

The second call passes an empty text, so saveNote calls back with an Error. The callback prints error: empty note and returns before it touches the result. Don't trust the result until you have checked err.

Node file functions use this style

In Node, fs.readFile("notes.txt", (err, text) => { ... }) reads a file and calls back in the error-first style. You don't need Node for this page. Just remember the rule: check err before you use text.

Callback hell: the pyramid of doom

REAL TIMERS

Sometimes one job must wait for another. For example, you need the user before you can load their orders. With callbacks, you start each next job inside the callback of the job before it.

Each new step adds more indentation, the space at the start of a line. So the code moves further right with every step. This shape is nicknamed callback hell, or the pyramid of doom. It causes three problems:

  • The code drifts to the right and gets hard to read.
  • Every step repeats the same error check.
  • The real story (user, then orders, then delivery) hides among the brackets.
Real-life analogyA to-do list inside a to-do list

Picture a to-do list where finishing one task shows you another list inside it. The work still gets done. But it is hard to see the whole plan, or to change one step without breaking another.

In real life: Each finished task shows you the next task
In JavaScript: Each callback starts the next job
In real life: The last task is buried deep inside
In JavaScript: The final success line is indented far to the right
In real life: Tasks on one flat list are easier to read
In JavaScript: Named functions keep the steps flat
Order a pizza with callbacks
Each step inside the one beforePop 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);      });    });  });}
What happened, in orderworked
  1. Press Run to order a pizza.
Try it yourself

Every step works: each callback starts the next step. The named version does exactly the same thing, without the pyramid shape.

Each step waits on a real setTimeout timer. Reset stops any waiting timers and goes back to Nothing fails.
Left: nested callbacks. getUser, getOrders, and getDelivery each sit inside the one before, so they step to the right. Right: named handlers. handleUser, handleOrders, and handleDelivery sit one under another.nested callbacksgetUser(…)getOrders(…)getDelivery(…)moves rightnamed handlershandleUserhandleOrdershandleDeliverytop to bottom
Left: each step sits inside the step before it, so the code moves right. Right: the same steps as named functions, read from top to bottom.

Named functions don't remove the callbacks. They only remove the pyramid shape: each step gets a name and sits at the left edge.

Promises, in the next lessons, go further. A promise turns "a result that arrives later" into a value you can return, chain, and catch errors on. The Promisification lesson shows how to turn this exact error-first style into promises.

Inversion of control: you hand someone the keys

INTERACTIVE

When you pass a callback, you let other code decide when your next step runs. This is called inversion of control: control moves from your code to the other code. You trust that code to call your callback:

  • once,
  • at the right time,
  • with the right values.

Built-in functions like setTimeout do this reliably. Code written by other people, called a library, might not. It could call your callback twice, too early, or never.

Real-life analogyHanding a builder your house keys

The keys let the builder finish the work while you are away. But now the builder decides when to come in. So you set rules: only one visit counts, and if nobody comes by a deadline, you treat that as a problem.

In real life: You give a builder your house 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 your callback to run exactly once
In real life: You set rules and a deadline
In JavaScript: Use once() and a time limit
Protect a callback you hand away
Two guards for a callback you hand awayPop 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);  };}
What your callback didguards on
  • callback: success
Try it yourself

The once guard ignores a second call. The time limit turns "nothing happened" into an error you can handle.

This library can't be trusted: pick how it calls your callback. Reset goes back to Calls once, with both guards on.

A once() guard is a small wrapper around your callback. The first call runs your callback. Every later call is ignored.

A time limit, or timeout, handles the other case. If nothing happens in time, it calls your callback with an error. Then your page doesn't wait forever.

Only the first call countsPop out in the code editor (opens in a new tab)JavaScript
function once(callback) {
  let called = false;
  return (value) => {
    if (called) return;
    called = true;
    callback(value);
  };
}

const logOnce = once((value) => console.log(value));
logOnce("first");
logOnce("second");

Line 2 makes a flag, called, set to false. Lines 3 to 7 return a new function that checks the flag. Line 11 calls it for the first time. The flag is still false, so it becomes true and first is printed. Line 12 calls it again. Now the flag is true, so line 4 returns and nothing prints.

The new function can still see called after once has finished. It keeps access to the variables around it; this is called a closure. The guard stops double calls. It can't fix a library that passes the wrong values, so check those values too.

Never mix sync and async callbacks (Zalgo)

STEP THROUGH

Some functions call your callback right away in some cases, and later in others. Programmers have a nickname for this: Zalgo.

A common cause is a cache, a store of answers you already have:

  • If the answer is in the cache (a cache hit), the function can call back right away.
  • If it isn't (a cache miss), the function must fetch the answer first, and call back later.

This uneven timing breaks the code that runs right after the call. That code can't know whether the callback has run yet.

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

When you order food in an app, you expect every update to come after you place the order. If the app sometimes sent an update while you were still paying, and sometimes later, you couldn't plan your next step.

In real life: Every update comes after you place the order
In JavaScript: The callback always runs later
In real life: One update arrives while you are still paying
In JavaScript: A cache hit calls back right away
In real life: Updates in the same order every time are easy to follow
In JavaScript: Always call back later in this function
Catch a callback that runs now or later
Step 0 of 6Ready
Your turn: follow the blue line

Guess whether the name prints before or after line 15's after. The fixed version always delays cache hits.

Running in
  1. script
Next: line 1
 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");
CallStoreChangeResultBlue line: runs next. Ran: already executed.
Recent returnsNothing yet. Start with the blue line.
Choose the name to look up, and whether to fix cache hits

Changing a choice starts the replay again. With the fix on, a hit and a miss print in the same order.

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.
Left: getName with ada is a cache hit. The console shows before, Ada Lovelace, after, because the callback runs now. Right: getName with grace is a cache miss. The console shows before, after, Grace Hopper, because the callback runs later.getName("ada")cache hitconsolebeforeAda Lovelaceaftercallback runs nowgetName("grace")cache missconsolebeforeafterGrace Hoppercallback runs later
The same function prints in two different orders. A cache hit calls back now; a cache miss calls back later.

The fix is not to make everything slow. The fix is to make it the same every time. If a function calls back later in one case, it should call back later in every case, even when the answer is in the cache.

queueMicrotask or setTimeout can delay the cache-hit callback. queueMicrotask(fn) runs fn as soon as the current script finishes. setTimeout(fn, 0) runs it a little later. Either one works here.

Where you'll use callbacks

Newer code often uses promises instead. But callbacks are still everywhere, because a function is how JavaScript says "do this later". You'll see them in:

  • event listeners, like a click on a button,
  • timers, like setTimeout,
  • array methods, like map and filter,
  • older Node functions, like fs.readFile,
  • test tools and libraries written by other people.

The table below lists each callback problem from this lesson. Read each row as: "this goes wrong, here is the callback fix, and here is how promises handle it."

Callback problems and how to fix them
What goes wrongThe fix with callbacksHow promises handle it (next lessons)
A result arrives later.Pass a callback like done(err, value).Return a promise that gives the value when it is ready.
Steps nest deeper and deeper.Give each step a named function, and return after errors.Chain the steps with .then(...), or use async functions.
The same error check repeats at every step.Use error-first callbacks every time.Use one .catch(...) at the end of the chain.
A library might call your callback twice.Wrap the callback with once().A promise can only finish once.
The callback sometimes runs now and sometimes later.Always call back later in async functions.Promise callbacks always run later.
Two kinds of callbacks that look alike

A completion callback belongs to one job, like reading one file. It should run once. An event listener is a callback for something that can happen many times, like clicks. It may run again and again. Use once for the first kind. Don't use it to block real clicks.

Read a file in Node, error firstPop 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);});

Line 3 starts reading profile.json and gives readFile a callback. readFile returns at once. Later, Node calls the callback with an error or with the file's text. Line 4 handles the error and stops. Line 6 turns the text into an object with JSON.parse, and line 7 prints the name.

You don't need Node for this page. But you'll see this pattern often: start a job, return now, and get an error or a result later.

Common misconceptions

"A callback always runs later."

No. A callback is any function you pass to another function to call. map calls its callback right away. Timers, events, and file reads call theirs later.

"setTimeout(fn, 0) runs right away."

No. It runs fn later. The current script always finishes first, even with a delay of 0.

"To report a failure from an async callback, throw an error."

No. Error-first functions report a failure by passing an error to the callback. If a later callback throws, the error can't reach the try block around the first call, because that call finished long ago.

"Callback hell is only about messy spacing."

No. The deep indentation is just the part you can see. The real problems are steps that are hard to follow, the same error check repeated, and inner steps you can't reuse.

"If the answer is in a cache, calling back right away is a harmless speed-up."

No. It changes when the callback runs, and the code after the call depends on that. If a function calls back later in one case, make it call back later in every case.

Practice: callbacks

5 EXERCISES
Exercise 1 · Warm-upPredict the order of the logs

Guess the order of the three logs, then run 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 · PracticeTurn the pyramid into named functions

    Take the pizza pyramid from the playground above. Rewrite it with named functions, one for each step.

      Exercise 3 · PracticeWrite an error-first callback

      Write a callback that handles the error first and the result second.

      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 its first call does anything.

        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

            For each question, first ask: does the callback run now, or later? Each answer explains why it is right or wrong.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What makes a callback async (run later)?

              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 job works?

              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 problem 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 with a value for a result that arrives later?

              Choose an answer to see the explanation.

            Key takeaways

            • A callback is a function you pass to another function. An async callback is called later, when a job finishes.
            • Error-first callbacks look like callback(err, result). Check the error first, and return from that branch before you use the result.
            • Callback hell is steps nested inside steps, with the same error check repeated. Named functions flatten it, and promises fix more of it.
            • Inversion of control means other code decides if and when your callback runs. A once() guard and a time limit protect you.
            • A function should never call back right away in some cases and later in others. That timing bug is called Zalgo.

            Remember the one-liner.
            A callback is a function you hand to other code, to call when the job is done.

            Up next: Promises, which fix many callback problems.

            CompleteFrontend Clear concepts. Working examples.