Callbacks in JavaScript: Why They Exist
If you’re just starting out with JavaScript, you might have heard the term "callback" tossed around like a hot potato. It sounds technical, but it’s actually based on a very simple idea: treating functions like ingredients.
If you've been writing JavaScript for a little while, you've almost certainly seen code like this:
button.addEventListener('click', function() {
console.log('Button clicked!');
});
That second argument — the function you pass into addEventListener — is a callback. You've probably been using callbacks without realizing there's a name for them, or understanding why they're designed the way they are.
This article explains callbacks from scratch: what they are, why JavaScript needs them, and what can go wrong when you use too many of them.
Functions are values in JavaScript
Before we can understand callbacks, we need to understand one unusual thing about JavaScript: functions are values, just like numbers or strings.
In most places where you can use a number like 42, you can also use a function. You can store a function in a variable, put it inside an array, or pass it to another function.
// A regular number stored in a variable
let age = 25;
// A function stored in a variable — same idea!
let greet = function() {
console.log('Hello!');
};
// Call it by adding () at the end
greet(); // prints: Hello!
Notice that greet without the parentheses is just the function sitting in memory — it hasn't done anything yet. Adding () is what actually runs it. This distinction matters a lot for callbacks.
💡 Key idea
A function name without
()is like holding a recipe card. A function name with()is like actually cooking the recipe.
Passing functions as arguments
Since functions are values, you can pass them into other functions. When you do, the receiving function can call it whenever it wants. The function you pass in is the callback.
// A simple function that calls whatever you give it
function doSomethingThenCall(myCallback) {
console.log('Doing something first...');
myCallback(); // call the function we were given
}
// Define a function to pass in
function sayDone() {
console.log('All done!');
}
// Pass sayDone as the callback
doSomethingThenCall(sayDone);
// prints: Doing something first...
// prints: All done!
See how we passed sayDone without parentheses? We're not calling it — we're handing it over so that doSomethingThenCall can call it at the right moment.
You can also write the callback function inline, directly where you need it. This is very common in JavaScript:
doSomethingThenCall(function() {
console.log('All done!');
});
// Same result — the function is defined right there, on the spot
This kind of function — defined on the spot with no name — is called an anonymous function.
Why callbacks exist: the async problem
So far callbacks look like a fancy way to call functions. Why not just call them directly? The answer is time. JavaScript often has to wait for things — and waiting creates a problem.
JavaScript runs one thing at a time
JavaScript is single-threaded, meaning it can only do one thing at a moment. If it stops to wait for something (like a file to load, or a server to respond), everything else — including your page updates — freezes completely.
So instead of waiting, JavaScript says: "Go do that slow thing, and when it's ready, call this function." That function is the callback.
📌 Analogy
Imagine ordering coffee at a café. The barista doesn't make you stand at the counter frozen until your drink is ready — they take your order and call your name when it's done. Your name being called is the callback.
Ex: setTimeout — the simplest async callback
console.log('Start');
setTimeout(function() {
console.log('This runs after 2 seconds');
}, 2000);
console.log('End');
// Output:
// Start
// End
// This runs after 2 seconds
Notice that "End" prints before the callback message, even though it comes after setTimeout in the code. JavaScript didn't wait — it moved on and ran the callback later when the timer expired.
Callbacks in real code
Now that you understand the concept, you'll recognize callbacks all over the place. Here are three you'll encounter constantly.
Event listeners: React to a user clicking a button
document.getElementById('myBtn')
.addEventListener('click', function() {
console.log('User clicked!');
});
// The callback runs only when the user actually clicks
Array methods: forEach, filter, map — all take callbacks
const numbers = [1, 2, 3, 4, 5];
// forEach — callback runs once for each item
numbers.forEach(function(n) {
console.log(n);
});
// filter — callback decides what to keep
const evens = numbers.filter(function(n) {
return n % 2 === 0;
});
// evens = [2, 4]
Fetching data from a server: Old-style XMLHttpRequest with a callback
function getData(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
callback(xhr.responseText);
};
xhr.send();
}
getData('/api/user', function(data) {
console.log('Got data:', data);
});
💡 Pattern to remember
Whenever you see
something(arg, function() { ... }), the function is a callback. It's telling JavaScript: "When you're done withsomething, run this."
The problem: callback nesting
Callbacks work great for a single async operation. But real applications often need to do several async things in sequence — and that's where callbacks start to hurt.
Imagine you need to: get a user → then get their orders → then get the order details → then show them. With callbacks, each step has to live inside the previous one:
Callback nesting (aka "Callback Hell")
getUser(userId, function(user) {
getOrders(user.id, function(orders) {
getOrderDetails(orders[0], function(details) {
showDetails(details, function() {
// We're 4 levels deep now...
console.log('Done!');
});
});
});
});
This shape — each level indented deeper than the last — is often called the Pyramid of Doom or Callback Hell.
The pyramid shape isn't just ugly — it makes code genuinely harder to work with:
⚠️ Problems with callback nesting
Hard to read. The logic is buried inside levels of indentation.
Hard to handle errors. You have to check for errors at every single level.
Hard to change. If one step changes, you have to reorganize the whole pyramid.
So what do we do about it?
JavaScript eventually introduced better tools for handling async operations: Promises and async/await. These solve the nesting problem and make async code look almost like normal, readable code.
But here's the important thing: callbacks came first, and they're what Promises and async/await are built on top of. Understanding callbacks gives you a solid foundation for everything that came after.
🎯 What to remember from this article
A callback is just a function you pass to another function, to be called later.
They exist because JavaScript can't wait for slow things — it needs a way to say "call me when it's done."
They show up everywhere: event listeners, array methods, timers, and HTTP requests.
When callbacks nest deeply, code becomes hard to maintain — which is the problem that Promises and async/await were designed to solve.