Callbacks
A function you pass into another function, to be called later — often once some work finishes.
What is it?
Functions are values in JavaScript, which means you can hand one function to another as an argument. A callback is exactly that: a function you pass into another function, with the expectation that it will be called at the right moment — after a click, after a timer finishes, after every item in an array.
You've already been using callbacks without necessarily naming them — the function you pass to .map(), addEventListener(), or setTimeout() is a callback.
Explain like I'm 10
It's like leaving a note with a restaurant host: 'Call this number when our table is ready.' You don't wait at the counter — you hand over instructions (the callback) and get on with your day, trusting it'll be used at the right moment.
Examples
A function that accepts a callback
function greetUser(name, onDone) {
const message = "Hi, " + name + "!";
onDone(message); // calling the callback with the result
}
greetUser("Amara", (message) => {
console.log(message); // "Hi, Amara!"
});Two different callbacks for success and failure
function checkAge(age, onAllowed, onDenied) {
if (age >= 18) {
onAllowed();
} else {
onDenied();
}
}
checkAge(
20,
() => console.log("Access granted"),
() => console.log("Access denied")
);
// logs "Access granted"A function can accept more than one callback — here, exactly one of the two runs, depending on the outcome.
How it works
Nothing special happens under the hood — a callback is just a regular function value, stored in a parameter and called like any other function, whenever the code inside decides to call it. The only thing that makes it a "callback" is the role it's playing: being called back later, by someone else's code, instead of being called directly by yours.
Why does it exist?
Callbacks let a function's behavior stay flexible without knowing the details in advance — .map() doesn't know what transformation you want, addEventListener doesn't know what should happen on click. The callback is how you supply that missing piece.
When to use it
Use a callback whenever you want to customize what happens at a specific point inside another function's logic — reacting to an event, running code once per array item, or defining what should happen once an asynchronous task finishes.
When not to use it
If you're nesting many callbacks inside each other for a sequence of asynchronous steps, that's exactly the pattern promises and async/await were built to replace — reach for those instead of deeply nested callbacks.
Common mistakes
Confusing
callbackwithcallback()— passing the function itself, not the result of calling it.Forgetting that a callback might run asynchronously, and expecting code after it to have access to its result immediately.
Nesting many callbacks inside each other, producing hard-to-read 'callback hell'.
Practice exercises
- Easy:
Write a function
runTwice(fn)that calls the given function twice in a row. - Medium:
Write a function
processArray(arr, callback)that calls callback once for every item (essentially rebuilding forEach). - Hard:
Write a function that takes a callback and calls it after a 1-second delay, using
setTimeout.
Interview questions
What is a callback function?
A function passed as an argument into another function, with the expectation that the receiving function will call it at the appropriate moment — after an event, after a delay, or once per item in a collection.
Are all callbacks asynchronous?
No — a callback just describes the role a function plays (being called by someone else's code), not when it runs. Callbacks passed to array methods like map/forEach run synchronously, immediately, during the call; callbacks passed to setTimeout or event listeners run later, asynchronously.
What does this log, in order? `console.log("start"); [1, 2, 3].forEach((n) => console.log(n)); console.log("end");`
start, 1, 2, 3, end — forEach's callback runs synchronously and completes entirely before the line after it executes; there's nothing asynchronous about it.
What does this log, in order? `console.log("start"); setTimeout(() => console.log("timeout"), 0); console.log("end");`
start, end, timeout — even a 0ms delay doesn't run the callback immediately; it's deferred to the event loop's task queue and only runs after all currently queued synchronous code (including console.log("end")) has finished.
What is the 'error-first callback' convention?
A pattern (common in Node.js-style APIs) where a callback's first parameter is reserved for an error object (null if none occurred) and the actual result comes second — (err, data) => {} — so callers check err before trusting data, and every callback handles errors the same consistent way.
What is 'callback hell', and what causes it?
A pyramid of deeply nested callbacks, each one representing the next step of a sequence of asynchronous operations. It happens because each step can only run inside the previous step's callback, and error handling has to be duplicated at every level — promises and async/await exist largely to flatten this back out.
What's the difference between passing `fn` and `fn()` as a callback?
fn passes the function itself, to be called later by the receiving code; fn() calls it immediately and passes whatever it returns instead — a very common bug when the intent was to defer the call.
What does this log? `function greet() { console.log("hi"); } setTimeout(greet(), 1000);`
"hi" is logged immediately, synchronously — greet() calls the function right there while building the arguments for setTimeout, and whatever it returns (undefined) is what actually gets scheduled, which does nothing a second later.
What's the bug here? `const counter = { count: 0, increment() { this.count++; } }; setTimeout(counter.increment, 0);`
Passing counter.increment hands over the bare function, detached from counter — when setTimeout calls it later, it's called as a plain function with no receiver, so this inside it isn't counter and this.count++ doesn't update what you expect. Fixing it needs .bind(counter) or wrapping it in an arrow function: () => counter.increment().
How do promises address the problems that deeply nested callbacks create?
A promise represents a single eventual result and lets you chain .then() calls at the same nesting level instead of inside one another, with errors propagating to a single .catch() instead of needing to be checked manually at every level.
How would you write a function that accepts an optional callback safely?
Check that it's actually a function before calling it — if (typeof callback === "function") callback(result); — or give the parameter a no-op default function, so calling the function without providing a callback doesn't throw.
What happens here? `function fetchData(callback) { const data = { id: 1 }; callback(data); } fetchData();`
It throws TypeError: callback is not a function. No argument was passed for callback, so it's undefined inside the function, and undefined can't be invoked — the function needed to guard against a missing callback before calling it.
Does a callback's return value always matter to the code that calls it?
It depends entirely on the caller. Array.prototype.map's callback return value builds the new array, and reduce's becomes the next accumulator — but a callback passed to forEach, addEventListener, or setTimeout has its return value completely ignored.
What's the difference between a 'callback' and an 'event handler'?
An event handler is really just a callback used in a specific role — a function registered to be called when a particular event (a click, a message, a timer) occurs. 'Callback' is the general term for any function handed to other code to be called later; 'event handler' names that specific use case.
Why does naming a callback function instead of passing it as an inline anonymous function sometimes help debugging?
A named function shows up by that name in stack traces and profiler output, and can be reused or tested independently — an anonymous inline callback just shows as <anonymous> in a trace, which makes tracking down which callback threw an error harder in code with many similar-looking inline callbacks.
If a function accepts multiple callbacks, like `onSuccess` and `onError`, what determines which one runs?
Whatever logic the function itself contains — the two callbacks are just ordinary parameters, and the function's own code decides, based on some condition it checks, which one (if any) it calls.