A Callback Is a Function You Hand to Another Function
Functions in JavaScript are values. You can store them in variables, put them in arrays, and — most importantly here — pass them as arguments. When you pass a function to another function so it can call it later, the function you passed is a callback.
greet doesn't know or care what formatter does. It just calls it with a name and uses the result. You pick the behavior by passing different callbacks. That flexibility is the whole reason callbacks exist.
Synchronous Callbacks Run Right Now
Not every callback is asynchronous. Many of the array methods you already use are callback-based, and they run the callback synchronously — before the outer call returns:
map, filter, and reduce all take a callback and invoke it once per element, right then. By the time map returns, every call to the callback has already happened. Nothing is queued for later.
This is a plain higher-order-function pattern — "here's some work, here's how to do it, give me the result." No event loop involved.
Asynchronous Callbacks Run Later
The callbacks people usually mean when they say "callbacks" are the async kind. You hand a function to some API that takes time — a timer, a network request, a file read — and the API calls your function back when the work finishes.
Output order: before, after, then timer fired a second later. setTimeout doesn't pause your program. It hands the callback to the runtime, returns immediately, and the rest of the script keeps going. A second later the event loop picks up the callback and runs it.
That "return now, call back later" shape is the mental model for every async callback API in JavaScript, from addEventListener to the older Node.js file APIs.
The Error-First Convention (Node.js)
Before promises existed, Node.js standardized on a specific callback shape: the first argument is an error (or null), the rest are the actual result. You'll still run into it in older code and some libraries.
The caller checks err first and bails out early if it's truthy. Only then does it trust the result. It's a convention — not enforced by the language — but once you see the (err, result) => ... signature you'll recognize it everywhere.
Callback Hell
The trouble starts when one async step depends on the result of another. Each callback has to nest inside the previous one, and you end up staircasing to the right:
This is the famous "pyramid of doom" or callback hell. A few things make it painful:
- The control flow zigzags instead of reading top-to-bottom.
- Every level repeats the same
if (err) return ...boilerplate. - One callback throwing an exception doesn't propagate to the outer ones — you have to handle errors at each layer.
- Refactoring means re-indenting the whole block.
You can flatten it somewhat by extracting named functions, but the core issue — async composition is clumsy with raw callbacks — doesn't go away. That's the problem promises were designed to solve.
Two Gotchas Worth Knowing
Don't call the callback by accident. When you pass a callback, you pass the function itself — not the result of calling it.
Watch out for this. If your callback is a regular function that uses this, the value of this depends on how the callback is called, not where it was defined. Arrow functions sidestep the problem by inheriting this from the surrounding scope:
Arrow functions are the default choice for inline callbacks for this exact reason.
Callbacks vs Promises
Callbacks still show up in synchronous APIs (map, forEach, sort), event listeners (element.addEventListener("click", ...)), and low-level runtime hooks. For async work that produces a single result, the ecosystem has moved almost entirely to promises.
The quick comparison:
- Callbacks — direct, minimal, but compose badly. Error handling is manual at every step.
- Promises — a value that represents a future result. Chain them with
.then(), handle errors once with.catch(), and the pyramid flattens.
You'll still need to understand callbacks: they're what promises are built on, and they're everywhere in event-driven code. But you rarely write new async APIs with raw callbacks anymore.
Next: Promises
Promises take the "do this when that's ready" idea and wrap it in an object you can pass around, chain, and compose. That's the next page — and the bridge to async/await, which is how most modern JavaScript handles async work.
Frequently Asked Questions
What is a callback function in JavaScript?
A callback is a function you pass as an argument to another function, so that function can call it later. setTimeout(() => console.log('hi'), 1000) passes an arrow function as a callback — setTimeout stores it and calls it when the timer fires. Callbacks are the original way JavaScript handled 'do this when that's ready.'
What's the difference between synchronous and asynchronous callbacks?
A synchronous callback runs immediately, during the call that received it — [1, 2, 3].map(x => x * 2) invokes the callback three times before map returns. An asynchronous callback is stored and invoked later, after some event happens — setTimeout, fs.readFile, and DOM event listeners all work this way. Async callbacks don't block the rest of your code.
What is callback hell and how do you avoid it?
Callback hell is the pyramid shape you get when async callbacks depend on each other and end up nested several levels deep. It makes control flow and error handling hard to follow. The fix is to use promises with .then() chains, or better, async/await — both flatten the pyramid back into something readable.