A callback is simply a function passed into another function as an argument, to be called (invoked) later — either immediately, or after some work finishes. This is possible because JavaScript treats functions as "first-class" values: they can be passed around and stored in variables just like a number or a string.
Callbacks are the foundation that asynchronous JavaScript was originally built on, which makes them worth understanding even though modern code often reaches for Promises instead.
A Simple Synchronous Callback
function processArray(arr, callback) {
const result = [];
for (const item of arr) {
result.push(callback(item));
}
return result;
}
function double(n) {
return n * 2;
}
console.log(processArray([1, 2, 3], double));
Output:
[ 2, 4, 6 ]
Here, double is the callback — processArray doesn't know or care what it does, it just calls it on each item. This is the same idea behind built-in array methods like map(), filter(), and reduce(), which all accept a callback function too.
Asynchronous Callbacks
Callbacks become especially important for operations that take time — a network request, a file read, or even just a deliberate delay. Instead of the program pausing and waiting, you hand over a callback that JavaScript will run once the operation finishes, while the rest of your code keeps running in the meantime.
console.log("1. Starting...");
setTimeout(() => {
console.log("3. This runs after 1 second");
}, 1000);
console.log("2. This runs immediately");
Output:
1. Starting...
2. This runs immediately
3. This runs after 1 second
Notice the order: line 2 prints before line 3, even though it appears after it in the code. setTimeout doesn't block execution — it schedules the callback to run later and lets the rest of the script continue immediately.
The Problem: Callback Hell
Things get messy once you need several async steps to happen in sequence, each depending on the last — you end up nesting callbacks inside callbacks:
getUser(id, (user) => {
getOrders(user.id, (orders) => {
getOrderDetails(orders[0].id, (details) => {
console.log(details);
// ...and it keeps growing to the right
});
});
});
This deeply nested, rightward-drifting shape is nicknamed "callback hell" — it's hard to read, hard to add error handling to, and easy to get wrong. This exact pain point is what Promises were introduced to solve, and later async/await made asynchronous code read almost like ordinary, top-to-bottom synchronous code.
