Callback function
A callback function is a function passed as an argument into another function, which calls it later, once it has finished its own work. Callbacks are normally used to run an action after an event, an operation or an asynchronous process, for example after data has loaded, a timer has fired or a click has happened.
Theory
TL;DR
- A callback is a function argument that is called by the function you handed it to, not by your own code.
- This works because functions in JavaScript are first class values: they can be passed around and returned.
- Callbacks can be synchronous (
map,filter,forEach) or asynchronous (setTimeout, event handlers, requests). - Pass a reference to the function, not its call:
setTimeout(fn, 100), notsetTimeout(fn(), 100). - Deeply nested callbacks turn into callback hell, which Promises and
async/awaitare there to cure.
Quick example
function fetchData(callback) {
setTimeout(() => {
const data = 'Data from the server';
callback(data); // call the function we were given
}, 1000);
}
fetchData(result => console.log(result)); // "Data from the server"fetchData knows nothing about what will be done with the result: it receives that part of the behaviour from outside.
Why it works: functions as values
In JavaScript a function is an ordinary value, so it can be assigned to a variable, put in an array or passed to another function. A function that accepts or returns another function is called a higher-order function, and the function that is passed in is the callback.
const numbers = [1, 2, 3];
// map is the higher-order function, the arrow function inside is the callback
const doubled = numbers.map(n => n * 2); // [2, 4, 6]Synchronous and asynchronous callbacks
A synchronous callback runs immediately, within the same function call. An asynchronous one is queued and runs later, once the call stack is empty.
// synchronous: runs before the next line
[1, 2, 3].forEach(n => console.log(n));
// asynchronous: runs after all synchronous code
setTimeout(() => console.log('later'), 0);
console.log('now');
// console order: 1, 2, 3, now, laterError handling: the error-first style
Node.js established a convention: the first parameter of a callback is the error, and the data follows it. That way the caller can handle a failure in the same place where it receives the result.
function loadUser(id, callback) {
setTimeout(() => {
if (!id) {
callback(new Error('id is required'));
return;
}
callback(null, { id, name: 'Maria' });
}, 100);
}
loadUser(7, (err, user) => {
if (err) {
console.error(err.message);
return;
}
console.log(user.name); // Maria
});Callback hell and the modern alternative
When one asynchronous operation depends on another, callbacks start nesting inside each other and the code drifts to the right like a staircase. That is callback hell: hard to read, and even harder to handle errors in.
loadUser(7, (err, user) => {
loadOrders(user.id, (err, orders) => {
loadItems(orders[0].id, (err, items) => {
console.log(items); // nesting grows with every step
});
});
});The modern answer is Promises and async/await: the same sequence is written flat, and errors are caught by a single try/catch.
async function showItems() {
try {
const user = await loadUser(7);
const orders = await loadOrders(user.id);
const items = await loadItems(orders[0].id);
console.log(items);
} catch (err) {
console.error(err.message);
}
}Common mistakes
- Passing the result of a call instead of the function itself:
setTimeout(sayHi(), 100)runssayHiright away and hands the timerundefined. The correct form issetTimeout(sayHi, 100). - Expecting a value received inside an asynchronous callback to be available right after the function call: it only appears once the callback actually runs.
- Ignoring the first error argument in error-first callbacks and carrying on with empty data.
- Calling the callback twice, both on success and on failure, because the
returnafter the error branch was forgotten. - Losing
thiswhen an object method is passed as a callback:element.addEventListener('click', obj.handler)calls the method with a differentthis. An arrow function orbindfixes it. - Building deeply nested callbacks where Promises or
async/awaitwould do the job.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.