← Back to Blog
Concurrency

Callbacks: a callback isn't necessarily async

Demystifying callbacks in JavaScript: the difference between synchronous and asynchronous callbacks, the role of the Event Loop, and the link to Inversion of Control.

📅 ✍️ Antoine Coulon
callbacksasyncevent-loopiocjavascript

There’s a stubborn belief among many JavaScript developers: that callback necessarily means asynchronous. That’s wrong, and the confusion muddies the understanding of mechanisms that are fundamental to the language. A callback is neither synchronous nor asynchronous by nature: it’s the context in which you invoke it that decides. Untangling this misunderstanding gives you the means to reason clearly about the Event Loop, about the order in which code executes, and about an architectural principle that is as understated as it is central: Inversion of Control.

The examples below are written in JavaScript, but the concepts they cover are universal: you’ll find them, in one form or another, in most languages.

What exactly is a callback?

A callback is simply a function passed as an argument to another function. Its purpose is to be executed later, at a moment controlled and determined by the function that receives it. Nothing more.

function runMain(callback) {
  // run the tasks [...]

  // once processing is done, invoke the callback
  callback("finished");
}

runMain((result) => {
  console.log(`program ended: ${result}`);
});

At this point, there’s no synchronous-versus-asynchronous debate. We’re only illustrating how a callback works: callback will be invoked when runMain decides to. The called function keeps control over the moment of invocation, and that’s exactly what makes the mechanism valuable.

Synchronous OR asynchronous, never by default

A callback can be synchronous or asynchronous, and it all depends on the context in which it’s invoked. The “or” is decisive here: a well-designed callback is one or the other, but never both at once. This nuance deserves a full treatment of its own, which I cover in the second part of this series, Callbacks: Why a Callback Should Never Be Both Synchronous and Asynchronous.

The synchronous callback

A synchronous callback is invoked before the function that uses it finishes executing. The call to the callback and the call to the function unfold on the same call stack: everything happens in a single, continuous execution flow, with no break.

/**
 * 1. Synchronous callbacks
 *
 * A synchronous callback is a callback that is invoked
 * before the function using it finishes executing.
 * Here, the callback is invoked synchronously over
 * every element of the array.
 */
console.log("Before");
[1, 2].forEach((n) => {
  console.log(`Callback invoked for: ${n}`);
  // [...]
});
console.log("After");

// Prints:
// "Before"
// "Callback invoked for 1"
// "Callback invoked for 2"
// "After"

The callback passed to forEach is invoked immediately, for each element of the array, before forEach returns control. The printed order confirms it: everything runs linearly, in the order of the code.

The asynchronous callback

An asynchronous callback, by contrast, is invoked once the function that uses it has finished executing. It’s therefore decoupled from the main flow and run later. This deferral can take several forms:

Asynchronous callbacks most often serve to handle I/O operations (reading a file, a network request, a database access) without blocking the main program while it waits.

/**
 * 2. Asynchronous callbacks
 *
 * An asynchronous callback is a callback that is
 * invoked independently, once the function
 * using it has finished executing.
 */
console.log("Before");
setTimeout(() => {
  console.log("Callback invoked");
}, 0);
console.log("After");

// Prints:
// "Before"
// "After"
// "Callback invoked"

This is where the Event Loop enters the picture. Even with a delay of 0, the setTimeout callback isn’t executed right away: it’s placed in the Event Loop’s Timer Queue and won’t be picked up until the current execution stack has been drained. Hence the printed order: "Before", then "After", and only after that "Callback invoked". setTimeout doesn’t “pause” the program; it schedules a future execution.

const timeout = setTimeout(() => {
  console.log("setTimeout callback");
}, 0);

console.log("after setTimeout function");

// Prints:
// 1. "after setTimeout function"
// 2. "setTimeout callback"

The same phenomenon plays out here: the synchronous line executes first, the callback of the setTimeout Web API second, because it passes through the Event Loop’s queue.

Beyond async: Inversion of Control

If a callback doesn’t need to be asynchronous, then what’s the point of using one in a purely synchronous context? The answer fits in three letters: IoC, for Inversion of Control.

A callback is one of the simplest ways to implement this principle, beautifully summed up by the “Hollywood Principle”: Don’t call us, we’ll call you back. You no longer control the moment of invocation; you delegate it to the function that receives your callback. It’s that function that decides when, and under what circumstances, your code will run.

Inversion of Control is applied by countless runtimes, frameworks, and libraries, often without you even realizing it. You implement your code, you honor the expected contract, and the actor in charge of control (the library, the runtime, the framework) handles execution at the right moment. The callback passed to forEach, an event handler, an HTTP server route: so many examples of IoC that have nothing to do with asynchronicity.

Conclusion

Callback and asynchronous are two distinct notions, and it’s time to stop conflating them. A callback is just a function handed off to be invoked later; it’s the invocation context, same call stack or Event Loop queue, that makes it a synchronous or asynchronous callback. Understanding this distinction means not only reasoning correctly about the order in which your code runs, but also recognizing in the callback one of the most elegant vehicles for Inversion of Control.

One essential question remains: why should a callback never be both synchronous and asynchronous? That’s the whole subject of the second part of this series.