Asynchronous JavaScript · Lesson 48 of 77 · practice

Callbacks, and why they exist

About 65 minutes.

By the end of this lesson you can

  • Explain what it means to pass a function to another function as an ordinary value.
  • Write a function that takes a callback, and say exactly when that callback runs.
  • Tell a synchronous callback from an asynchronous one by asking whether the outer function has returned yet.
  • Write and read the error-first callback shape, handling the failure branch before the success branch.
  • Explain why deeply nested callbacks get hard to read, and what promises were invented to fix.

A function is a value, like 7 or "hello"

Explanation

You already store numbers in variables. JavaScript treats a function the same way. When you write `function shout(text) { ... }` you have not only described some behaviour, you have created a value and given it the name `shout`. You can copy it into another variable, put it in an array, or hand it to another function — all without calling it.

The difference between `shout` and `shout("hi")` is the whole idea. `shout` is the function itself: the recipe, sitting on the shelf. `shout("hi")` is the result of running it: the meal. Write the parentheses and it runs right now. Leave them off and you are just passing the recipe along.

A function that you hand to another function, so that the other function can call it, is called a callback. That is the entire definition. It is not a special kind of function and there is no keyword for it — it is a job description, not a type.

Leaving your number at the counter

An analogy

Imagine ordering at a busy bakery. One option is to stand at the counter until your bread comes out. You get the bread, but you cannot do anything else while you wait, and the queue behind you cannot move.

The other option is to leave your phone number and go and do your shopping. The bakery has your number — it has your callback. When the bread is ready, they call you. You did not have to stand there, and the queue kept moving.

That second shape is what callbacks buy you. You say what should happen with the answer, hand that instruction over, and get on with something else. The catch — the thing this whole module exists to teach — is that the bakery decides when to phone, not you.

"Callback" does not mean "later"

Explanation

This trips up nearly everybody, so read it twice: most callbacks in JavaScript run immediately. `[1, 2, 3].map(n => n * 2)` takes a callback and calls it three times before `map` returns. Nothing is delayed. Nothing is scheduled. It is an ordinary function call inside a loop.

MDN puts callbacks in two groups. A synchronous callback is called during the outer function's own run, with nothing asynchronous in between. An asynchronous callback is called at some point later, after some operation has finished. Same word, two completely different timings, and the word alone will not tell you which one you have.

This matters more than it sounds. If a callback is synchronous, you can be sure it has finished by the time the outer call returns, so anything it assigned is ready to use on the next line. If it is asynchronous, the next line runs first and that variable is still empty. Reaching for a value that has not arrived yet is the single most common asynchronous bug there is.

The one question that settles it

A mental model

When you meet a function that takes a callback, ask one question: has the outer function already returned by the time my callback runs?

If the answer is no — the callback runs while the outer function is still on its way — it is synchronous. `map`, `filter`, `forEach`, `sort` and every function you write yourself in this lesson are in this group.

If the answer is yes — the outer function returned long ago and your callback fires afterwards — it is asynchronous. Timers, network requests and file reads are in this group. You cannot tell which group you are in by looking at the callback. You have to know what the outer function does, which is why the documentation for that function is the thing to read.

A cheap way to check in real code: print something right after the call. If your callback's output appears before that line, it is synchronous. If it appears after, it is not. You will do exactly that in the worked examples below.

The error-first shape

Explanation

There is a convention you will meet constantly, especially in older code and in Node.js: the callback takes two parameters, and the first one is the error. Success calls `callback(null, value)`. Failure calls `callback(someError)`.

The reason for the convention is that an asynchronous function cannot report failure the way an ordinary function does. By the time the network request fails, the function that started it has already returned, so there is nobody left to `throw` at — the `try` block that surrounded the original call finished ages ago. The error has to travel the same route as the answer: through the callback.

Because the error comes first, the honest way to write the handler is to deal with it first: `if (error) { ...; return; }` and only then touch the value. Putting the error check first, with an early return, is what stops the success path from running on data that was never produced.

Four mistakes that bite

A common mistake

Calling instead of passing. `eachItem(list, myHandler())` runs `myHandler` immediately and passes its return value — usually `undefined` — as the callback. You wanted `eachItem(list, myHandler)`, with no parentheses. If you get "is not a function" from inside the outer function, look here first.

Ignoring the error argument. `function (error, value) { console.log(value); }` looks fine and hides every failure, because on failure `value` is `undefined` and nothing complains. Check `error` first or do not accept it at all.

Calling the callback twice. Forgetting a `return` after the failure branch means execution falls through and the success branch calls the callback again. The caller then handles one request twice, which is much harder to debug than a crash.

Assuming the callback already ran. `let result; getThing(cb); console.log(result);` prints `undefined` for every asynchronous `getThing`. The fix is never to move the `console.log` around until it works — it is to move the code that needs `result` inside the callback.

Why this lesson comes first

Why it matters

Promises and `async`/`await`, which fill the rest of this module, are not a replacement for the idea of a callback — they are a better container for it. `.then(handleResult)` is still handing a function over for somebody else to call. If passing functions around feels solid, the rest of the module is a change of spelling. If it does not, promises will feel like magic, and magic is impossible to debug.

There is also a practical reason. Nesting callbacks works for one step and gets unreadable at three: each step adds a level of indentation and repeats the same error check, and there is no way to say "do these two at the same time" without writing a counter by hand. Promises exist because of those specific pains. You will appreciate the cure much more having felt the disease.

Recap

Recap

A function is a value; pass it without parentheses. A callback is just a function that somebody else calls. Most callbacks run immediately — ask whether the outer function has returned yet, and prove it with a print statement rather than guessing. Error-first callbacks put the failure in the first parameter, so handle it first and return. Everything after this lesson is the same idea in a tidier package.

Worked examples

Every output below was produced by actually running the code in the same sandbox your exercises run in — it is not a prediction.

Passing a function instead of calling it

Shows that a function is a value you can hand over. `announce` does not know or care what `style` does; it just calls whatever it was given. Notice that `shout` and `whisper` are passed with no parentheses.

1function shout(text) {2  return text.toUpperCase() + "!";3}4 5function whisper(text) {6  return text.toLowerCase() + "...";7}8 9function announce(name, style) {10  return style("the winner is " + name);11}12 13console.log(announce("Ada", shout));14console.log(announce("Ada", whisper));15 
  • line 9

    `style` is an ordinary parameter that happens to hold a function. Calling it with `style(...)` is the only thing that makes it special.

  • line 12

    `shout` with no parentheses passes the function itself. `shout(...)` here would pass its result instead, and then `style(...)` would fail with "not a function".

What it prints

THE WINNER IS ADA!the winner is ada...

Writing your own function that takes a callback

Builds a tiny version of `forEach` and proves, with print statements, that the callback runs while `eachItem` is still running. Read the output order before reading the explanation — it is the whole point.

1function eachItem(list, callback) {2  console.log("eachItem: starting");3  for (let index = 0; index < list.length; index++) {4    callback(list[index], index);5  }6  console.log("eachItem: finished");7}8 9console.log("before the call");10eachItem(["milk", "eggs"], function (item, index) {11  console.log("callback saw " + index + ": " + item);12});13console.log("after the call");14 
  • line 4

    `eachItem` decides how many arguments the callback receives. Here it hands over the item and its index — exactly what the built-in `forEach` does.

  • line 10

    The function passed here has no name. It is still just a value, created on the spot and handed straight over.

  • line 13

    This line prints LAST. Everything the callback printed appears above it, which is what proves the callback was synchronous.

What it prints

before the calleachItem: startingcallback saw 0: milkcallback saw 1: eggseachItem: finishedafter the call

The error-first shape, running synchronously

The same two-parameter shape you will meet in asynchronous code, but with the timing removed so you can concentrate on the shape. `report` is written once and reused for three different outcomes.

1function parseAge(text, callback) {2  const value = Number(text);3  if (text.trim() === "" || Number.isNaN(value)) {4    callback(new Error("Not a number: " + text));5    return;6  }7  if (!Number.isInteger(value) || value < 0) {8    callback(new Error("Not a whole age: " + text));9    return;10  }11  callback(null, value);12}13 14function report(error, age) {15  if (error) {16    console.log("failed: " + error.message);17    return;18  }19  console.log("ok: " + age);20}21 22parseAge("41", report);23parseAge("forty", report);24parseAge("-3", report);25 
  • line 5

    Without this `return`, execution would carry on to line 11 and call the callback a second time — with a success this time. One outcome, one call.

  • line 11

    Success passes `null` as the error. Not `undefined`, not a missing argument: an explicit "nothing went wrong" is easier to check and easier to read.

  • line 15

    The failure branch comes first and returns. The success code below it can then assume `age` is real.

What it prints

ok: 41failed: Not a number: fortyfailed: Not a whole age: -3

The same shape, but the answer arrives later

Identical structure to `parseAge`, with one difference: the callback is handed to a timer instead of being called directly. That single change moves the callback from synchronous to asynchronous, and the printed order changes with it.

1function fetchGreeting(name, callback) {2  setTimeout(function () {3    callback(null, "Hello, " + name);4  }, 50);5}6 7console.log("1. asking");8fetchGreeting("Ada", function (error, greeting) {9  if (error) {10    console.log("failed: " + error.message);11    return;12  }13  console.log("3. " + greeting);14});15console.log("2. asked, and still running");16 
  • line 2

    `fetchGreeting` returns as soon as the timer is booked. It does not wait 50 ms; nothing waits.

  • line 15

    This prints SECOND, before the greeting. Compare with the previous example, where the line after the call printed last.

This one does not run hereThis sandbox has no timers of any kind — `typeof setTimeout === "undefined"` (verified). Read it instead: in a browser or in Node the printed order is 1, then 2, then 3 about 50 ms later.

Why three steps get unreadable

Three dependent asynchronous steps written with callbacks. Do not try to follow the logic — look at the shape. Every step adds a level of indentation and repeats the same error check, and the final action ends up furthest from the left margin.

1loadUser(7, function (userError, user) {2  if (userError) {3    return showError(userError);4  }5  loadOrders(user.id, function (orderError, orders) {6    if (orderError) {7      return showError(orderError);8    }9    loadInvoice(orders[0].id, function (invoiceError, invoice) {10      if (invoiceError) {11        return showError(invoiceError);12      }13      render(invoice);14    });15  });16});17 
  • line 13

    The one line that does the actual work sits four levels deep. Adding a fourth step pushes it deeper still.

  • line 6

    The same three lines of error handling appear at every level. There is no way to write them once.

This one does not run hereThe three loader functions are asynchronous and do not exist here; the sandbox has no network or filesystem. The point is the shape, not the result — this is the pain that promises were designed to remove.

Check yourself

Not scored, not stored. Getting one wrong is the useful part.

You read that a library function "takes a callback". What does that tell you about when your function will run?

`twice` calls its callback two times. Before running it, write down the four lines you expect, in order.

function twice(value, callback) {  callback(value);  callback(value);}console.log("start");twice("ping", function (text) {  console.log(text);});console.log("end");

In an error-first callback `function (error, value) { ... }`, why is the error checked first and followed by a `return`?

Write it yourself

Exercise · intro · 6 tests

Write a function that takes a callback

Write `applyToEach(items, transform)`. It should return a NEW array holding the result of calling `transform` on every item, in order. Call `transform` with two arguments: the item, and its index. Do not use `Array.prototype.map` — writing the loop by hand is the point, because it makes it obvious that YOUR function is the one calling `transform`. The original array must be left exactly as it was.

What your code must do

Returns a new array of the same length as `items`, where entry `i` is `transform(items[i], i)`. Order matters and is the input order. `items` itself is never modified and is never the value returned, even when it is empty. `transform` is called exactly once per item and never for an empty array.

Define applyToEach at the top level of your code — the tests call it by name.

Runs on your device in a sandbox with no network and no access to this page.

Exercise · core · 8 tests

Answer with an error-first callback

Write `getSetting(settings, key, callback)`. If `settings` has the property `key`, call `callback(null, settings[key])`. If it does not, call `callback(new Error("Unknown setting: " + key))` and pass nothing as the second argument. Call the callback exactly once on either path, and call it before `getSetting` returns. `getSetting` itself returns nothing. Careful: `0` and the empty string are perfectly good settings values, so the presence of a key is not the same question as whether its value is truthy.

What your code must do

Exactly one callback call per invocation. Success passes `null` first and the stored value second, including when that value is falsy. Failure passes an `Error` whose message is "Unknown setting: " followed by the key, and leaves the second argument `undefined`. The callback runs synchronously, before `getSetting` returns.

Define getSetting at the top level of your code — the tests call it by name.

Runs on your device in a sandbox with no network and no access to this page.

Worth remembering

  • What is a callback?

    A function passed to another function as a value, so that the other function can call it. It is a job description, not a special kind of function — and it says nothing about when the call happens.

  • How do you tell a synchronous callback from an asynchronous one?

    Ask whether the outer function has already returned by the time the callback runs. Print a line right after the call: if the callback's output lands above it, the callback was synchronous.

  • What does the error-first callback convention look like, and why does it exist?

    `callback(null, value)` on success, `callback(error)` on failure. It exists because an asynchronous function has already returned by the time it fails, so there is nothing left to `throw` at — the error has to travel through the callback like any other result.

Check this lesson against the source

We wrote the explanation; we did not invent the facts. This is the page that backs them.

MDN — Callback function (Glossary)

Check there: That a callback is a function passed into another function and invoked inside it, and that callbacks are called in two ways — synchronously, immediately after the outer function is invoked, or asynchronously, after an operation completes.

MDN — Array.prototype.forEach()

Check there: A callback-taking function in the standard library, and the note that forEach expects a synchronous function and does not wait for promises.

MDN — Functions (JavaScript Guide)

Check there: That functions are values which can be assigned and passed as arguments.

Prefer to read the source in a structured breakdown? Turn this doc into a breakdown

0 of 2 exercises in this lesson solved on this device

Consent version 2026-07-31.1

Cookie preferences