Skip to main content

Command Palette

Search for a command to run...

Callbacks in JavaScript: Why They Exist

Published
3 min read

JavaScript is a language where functions are not just reusable blocks of code—they are first-class citizens. This means functions can be treated like values: stored in variables, passed as arguments, and returned from other functions.

This capability is the foundation of callbacks, one of the most important concepts in JavaScript.


🔹 Functions as Values in JavaScript

In JavaScript, functions behave like any other variable.

function greet() {
  console.log("Hello!");
}

const sayHello = greet;

sayHello(); // Hello!

You can also pass functions into other functions:

function execute(fn) {
  fn();
}

execute(greet); // Hello!

This is where callbacks begin.


🔹 What is a Callback Function?

A callback function is simply a function that is passed as an argument to another function and is executed later.

function greet(name, callback) {
  console.log("Hi " + name);
  callback();
}

function sayBye() {
  console.log("Goodbye!");
}

greet("Sayantan", sayBye);

Output:

Hi Sayantan
Goodbye!

👉 Here, sayBye is the callback function.


🔹 Why Callbacks Exist (Core Reason)

JavaScript is single-threaded, meaning it can do only one thing at a time.

But real-world tasks often take time:

  • Fetching data from an API

  • Reading a file

  • Waiting for user input

If JavaScript waited for each task to finish, your app would freeze.

❌ Problem Without Callbacks

const data = fetchData(); // takes 3 seconds
console.log(data);

This blocks everything.


✅ Solution: Asynchronous Programming with Callbacks

function fetchData(callback) {
  setTimeout(() => {
    callback("Data received");
  }, 3000);
}

fetchData(function(result) {
  console.log(result);
});

Output (after 3 seconds):

Data received

👉 Instead of waiting, JavaScript continues running and executes the callback when the task is done.


🔹 Passing Functions as Arguments

Callbacks rely on passing functions into other functions.

function processUserInput(callback) {
  const name = "Sayantan";
  callback(name);
}

processUserInput(function(name) {
  console.log("Hello " + name);
});

This pattern is extremely powerful because it allows dynamic behavior.


🔹 Common Use Cases of Callbacks

1. Event Handling

document.getElementById("btn").addEventListener("click", function() {
  console.log("Button clicked!");
});

👉 The function runs only when the event happens.


2. Timers

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

3. Array Methods

const numbers = [1, 2, 3];

numbers.forEach(function(num) {
  console.log(num);
});

👉 forEach takes a callback and runs it for each element.


4. API Calls / Async Operations

fetchData(function(data) {
  console.log("Processing:", data);
});

🔹 The Problem: Callback Nesting (Callback Hell)

When multiple async operations depend on each other, callbacks get nested.

getUser(function(user) {
  getOrders(user, function(orders) {
    getDetails(orders, function(details) {
      console.log(details);
    });
  });
});

This leads to:

  • Hard-to-read code

  • Difficult debugging

  • Poor maintainability

👉 This is known as Callback Hell or the Pyramid of Doom.


🔹 Why This is a Problem Conceptually

The issue is not callbacks themselves—it’s deep nesting and dependency chains.

Problems include:

  • Loss of readability

  • Error handling becomes messy

  • Code becomes tightly coupled


🔹 What Came After Callbacks?

To solve these problems, JavaScript introduced:

  • Promises

  • Async/Await

These make asynchronous code:

  • Cleaner

  • Easier to read

  • More maintainable