Skip to main content

Command Palette

Search for a command to run...

Callbacks in JavaScript: Why They Exist

Published
4 min read
Callbacks in JavaScript: Why They Exist

In JavaScript, functions are first-class citizens. This means they can be treated like any other value:

  • Assigned to variables

  • Stored in arrays or objects

  • Passed as arguments to other functions

  • Returned from functions

const greet = function(name) {
  console.log("Hello, " + name);
}; // function can be assigned to variables

const functions = [greet, (x) => x * x]; 
// They can be stored in data structures

Because function are the values. Can be passed to other function — this is the essence of the callback.


What a callback function is

Callback is a function that is passed as argument to another function and is executed later, often after some operation has completed.

Simple Synchronous Example

const processUserInput (name, callback){
    // do some processing 
    const processedName = name.trim().toUpperCase();
    // then use the callback with the result
    callback(processedName );
}

function greetUser(name) {
  console.log(`Hello, ${name}!`);
}

processUserInput("  Alia ", greetUser);

// Output: Hello, ALIA!

Here, greetUser is the callback. The processUserInput function calls back the provided function after its own work is done.


Why Callbacks? The Role of Asynchronous Programming

JavaScript is single‑threaded. If a task takes a long time (e.g., reading a file, waiting for a network response), it would block the entire program. To avoid this, JavaScript uses asynchronous operations that run in the background and notify the program when they finish—often via callbacks.

Example: setTimeout

console.log("Start");

setTimeout(() => {
  console.log("Callback executed after 2 seconds");
}, 2000);

console.log("End");
// Output:
// Start
// End
// (after 2 seconds) Callback executed after 2 seconds

The callback passed to setTimeout runs later, without blocking the rest of the code. This non‑blocking behaviour is why callbacks are indispensable in JavaScript.

Example: Event Listener

document.getElementById("btnid12".addEventListener("click", () => {
     console.log("Button was clicked!");
})

The callback runs only when the user clicks the button—again, an asynchronous event.


Passing Functions as Arguments (Higher‑Order Functions)

Functions that accept other functions as arguments (or return them) are called higher‑order functions. Callbacks are a classic example.

Array Methods

Array methods like map, filter, and forEach rely on callbacks:

const numbers = [1, 2, 3, 4];

const doubled = numbers.map(function(num) {
      return num * 2;
});
console.log(doubled); // [2, 4, 6, 8]

Here the anonymous function is the callback that map calls for each element.

Why This Matters

Passing functions as arguments allows separation of concerns. The higher‑order function handles the repetitive logic (like iterating over an array), while the callback defines the custom operation.


Common Scenarios Where Callbacks Are Used

Timers: setTimeout, setInterval

DOM Events: addEventListener, onclick, etc.

Network Requests: fetch (with .then is Promise‑based, but under the hood callbacks are used; also older XMLHttpRequest)

File System: Node.js fs.readFile uses callbacks

Database Queries: In Node.js, many database libraries use callbacks

Custom Higher‑Order Functions: You can create your own functions that accept callbacks

Node.js Example (File Reading)

const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('Error reading file:', err);
    return;
  }
  console.log('File content:', data);
});

console.log('Reading file...'); // Runs before the file is read

The Problem: Callback Nesting (Callback Hell)

When you have multiple asynchronous operations that depend on each other, you end up nesting callbacks inside callbacks. This quickly becomes hard to read and maintain—a phenomenon known as callback hell or the pyramid of doom.

Example of Callback Nesting

getUser(1, (user) => {
  console.log('User:', user);
  getOrders(user.id, (orders) => {
    console.log('Orders:', orders);
    getOrderDetails(orders[0].id, (details) => {
      console.log('Details:', details);
      // More nested operations...
    });
  });
});

Why It’s Problematic

  • Readability: The code indents deeper with every asynchronous step.

  • Error Handling: Each callback must handle errors individually; there is no central place.

  • Maintainability: Adding, removing, or modifying steps is error‑prone.

  • Reusability: Hard to extract and reuse pieces of logic.

Conceptual Diagram: Nested Callback Execution

Each operation waits for the previous one, and the nesting creates a deep chain.


Conclusion

Callbacks are a direct consequence of JavaScript’s function‑as‑value nature and its need for non‑blocking asynchronous operations. They enable powerful patterns like event handling, iteration, and customisable behaviour in higher‑order functions. However, when used for sequential asynchronous tasks, they can lead to deeply nested code that is hard to manage.

Modern JavaScript offers Promises and async/await to address these nesting issues while still relying on the same callback concept under the hood. Nevertheless, callbacks remain essential to understand, as they form the foundation of JavaScript’s concurrency model and appear in countless APIs.