Skip to main content
Madhukar
All Articles

Callbacks in JavaScript: Why They Exist

August 3, 20265 min read
JavaScriptAsyncCallbacksFunctional Programming
Callbacks in JavaScript: Why They Exist

Functions as values — the idea everything else builds on

In JavaScript, a function isn’t just something you call — it’s something you can hold, pass around, and store, exactly like a number or a string.

const greetLoudly = function () {
console.log("HELLO!");
};

const myFunction = greetLoudly; // stored in a variable, just like any other value

This single idea — that functions are values, not just actions — is the entire foundation callbacks are built on. Once a function can be handed to another piece of code, a whole new set of patterns becomes possible.

What a Callback Function Is

A callback is simply a function passed as an argument to another function, to be called (executed) at some point by that other function.

function greet(callback) {
console.log("Getting ready...");
callback();
}

function sayHi() {
console.log("Hi!");
}

greet(sayHi);
// "Getting ready..."
// "Hi!"

Nothing exotic is happening here — sayHi is just a function, handed to greet as a value (not called with parentheses, just passed by name), and greet decides when to actually run it.

Why Callbacks Are Used in Asynchronous Programming

The example above is synchronousgreet calls sayHi immediately, in order, with no waiting involved. But callbacks become genuinely essential once time enters the picture — when something won't be ready instantly.

console.log("Start");

setTimeout(function () {
console.log("This runs after 2 seconds");
}, 2000);

console.log("End");
Start
End
This runs after 2 seconds

Notice the order: "End" logs before the delayed message, even though the delayed callback was written earlier in the code. setTimeout doesn't pause the program to wait — it schedules the callback to run later, and JavaScript moves on immediately to whatever comes next.

This is exactly why callbacks matter for asynchronous work: they give JavaScript a way to say “run this specific code once this other thing eventually finishes” — without needing to pause and wait idly in the meantime.

Passing Functions as Arguments

Callbacks show up constantly, in situations far beyond timers. A few common, practical examples:

Responding to a click

button.addEventListener("click", function () {
console.log("Button was clicked!");
});

The function passed to addEventListener doesn't run immediately — it's stored and called later, specifically when the button is actually clicked.

Running code for each item in an array

[1, 2, 3].forEach(function (num) {
console.log(num * 2);
});
// 2
// 4
// 6

Here, the callback runs once for every item in the array — forEach handles the looping, and calls your function each time with the current item.

Reading a file (Node.js)

fs.readFile("data.txt", "utf8", function (err, data) {
console.log("File contents:", data);
});

The callback runs only once the file has actually finished being read — exactly the same underlying pattern as setTimeout, just applied to a different kind of delayed operation.

Basic Problem of Callback Nesting

Callbacks work cleanly for one step. The trouble starts when several asynchronous steps depend on each other, and each one is expressed as a callback nested inside the previous one:

setTimeout(() => {
console.log("Step 1 done");

setTimeout(() => {
console.log("Step 2 done");

setTimeout(() => {
console.log("Step 3 done");
}, 1000);
}, 1000);
}, 1000);

Each new step indents further to the right, and the actual sequence of events gets buried inside deeper and deeper layers of nested functions.

Why this becomes a real problem

  • Readability drops as each additional step adds another layer of indentation
  • Tracing the actual order of execution means mentally unwinding several nested closing braces
  • Adding, removing, or reordering a step means carefully restructuring nested functions, not just moving a line

This specific shape — callbacks nested inside callbacks inside callbacks — is widely nicknamed “callback hell,” and it’s precisely the problem that later tools (Promises, and eventually async/await) were built to solve, without changing the underlying asynchronous behavior itself.

Final Takeaway

Callbacks exist because of one simple, foundational fact about JavaScript: functions are values, which means they can be handed to other code and called later, whenever that other code decides the time is right. That’s all a callback really is — for a simple synchronous example, or for something genuinely asynchronous like a timer, a click, or a file read finishing. The pattern only becomes awkward when several dependent asynchronous steps stack callbacks inside callbacks, producing the deeply nested, hard-to-follow shape known as callback hell — a real, well-known problem, but one that stems from how callbacks get combined, not from the basic idea of a callback itself.

Frequently Asked Questions

Is every function passed as an argument automatically a callback?

> Yes — any function handed to another function as an argument, to be called by that function, is a callback, whether the situation is synchronous (like the greet/sayHi example) or asynchronous (like setTimeout).

Why does JavaScript run “End” before the setTimeout callback, even though it’s written afterward?

> Because setTimeout doesn't pause the program — it schedules the callback to run later and lets the rest of the code continue immediately. The callback only runs once its delay has passed, which is why it appears last in the output despite being written earlier in the code.

Do I need Promises to avoid callback problems entirely?

> Promises (and async/await, built on top of them) are the modern, more readable way to handle multiple dependent asynchronous steps, specifically because they avoid the deep nesting callbacks can produce. Callbacks themselves are still used constantly under the hood, even in code that primarily uses Promises.

Are callbacks only relevant for asynchronous code?

> No — as the greet/sayHi example shows, callbacks work perfectly well synchronously too. They become especially important for asynchronous code specifically because that's where "run this later, once something finishes" genuinely matters.

Originally published by Mr Madhukar

Read the complete article on Medium with full formatting & reader responses.