Skip to main content

Command Palette

Search for a command to run...

Callback Functions in JavaScript: Passing Functions Like Values

Updated
15 min read
Callback Functions in JavaScript: Passing Functions Like Values

Who is this for? If you have ever seen a function passed inside another function and wondered what that means — or if words like "callback" feel confusing — this guide explains everything from scratch with simple, practical examples.


Table of Contents

  1. What Is a Callback Function?

  2. Why Callbacks Are Used in Asynchronous Programming

  3. Passing Functions as Arguments

  4. Callback Usage in Common Scenarios

  5. The Basic Problem of Callback Nesting

  6. Diagrams

  7. Quick Reference


Introduction

In most programming languages, you call a function, it runs, and that is that. JavaScript goes further — it lets you treat functions as values. You can store a function in a variable, pass it into another function, and even return it from a function.

This one idea — functions as values — is what makes callbacks possible. And callbacks are everywhere in JavaScript: inside setTimeout, addEventListener, map(), filter(), file reading, API calls, and more.

Once you truly understand callbacks, a huge portion of JavaScript code that looked mysterious will suddenly make complete sense.

💡 Open your browser console (F12 → Console tab) and try every example as you read.


1. What Is a Callback Function?

A callback function is a function that is passed as an argument into another function, to be called (executed) at a later point.

The name says it plainly: you pass a function in, and that function gets called back when the time is right.

The simplest possible example

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

function processUser(name, callback) {
  console.log("Processing user...");
  callback(name); // calling the function that was passed in
}

processUser("Priya", greet);
// Output:
// Processing user...
// Hello, Priya!

Here greet is the callback. Notice:

  • greet is passed without parenthesesprocessUser("Priya", greet)

  • If you wrote greet() with parentheses, you would call it immediately and pass its result, not the function itself

  • processUser decides when to call it by doing callback(name)

Functions are values — the key insight

In JavaScript, a function is just a value, the same way a number or a string is a value. You can:

// Store a function in a variable
const sayHi = function() {
  console.log("Hi!");
};

// Pass a function to another function
setTimeout(sayHi, 1000); // sayHi will be called after 1 second

// Store a function in an array (yes, really)
const actions = [sayHi, function() { console.log("Bye!"); }];
actions[0](); // "Hi!"
actions[1](); // "Bye!"

Once you accept that functions are values in JavaScript, callbacks stop being mysterious — they are just a function being handed to another function as an argument.

Callback using an anonymous function

Instead of defining the function separately, you can write it inline:

function processUser(name, callback) {
  console.log("Processing user...");
  callback(name);
}

// Passing an anonymous function directly
processUser("Rahul", function(name) {
  console.log("Welcome, " + name + "!");
});

// Output:
// Processing user...
// Welcome, Rahul!

Both approaches do the same thing. The inline (anonymous) version is just more compact.


2. Why Callbacks Are Used in Asynchronous Programming

JavaScript runs one thing at a time

JavaScript is single-threaded — it can only execute one piece of code at a time. If a task takes a long time (reading a file, calling an API, waiting for a timer), and JavaScript just sat and waited for it, your entire application would freeze.

Callbacks solve this by saying: "Start this slow task, and when it finishes — call this function to handle the result."

The problem without callbacks

Imagine reading a file synchronously (blocking):

// Synchronous — the entire program freezes here
const data = readFileSynchronously("users.txt");
console.log(data);

console.log("This cannot run until the file is fully read.");
// If the file takes 3 seconds, nothing happens for 3 seconds

In a web server, this would mean no other user request could be handled during those 3 seconds. That is unacceptable.

The solution — async + callback

const fs = require("fs");

// Asynchronous — does NOT block
fs.readFile("users.txt", "utf8", function(error, data) {
  // This callback runs ONLY when the file is ready
  if (error) {
    console.log("Could not read file:", error);
    return;
  }
  console.log("File contents:", data);
});

// This runs IMMEDIATELY — does not wait for the file
console.log("File is being read in the background...");

Output:

File is being read in the background...
File contents: Priya, Rahul, Ananya   ← appears after the file is read

The callback acts as your instruction: "When the file is ready, here is what to do with it." JavaScript does not have to wait — it continues running other code and comes back to the callback once the file is ready.

The async flow in plain English

1. "Start reading the file."
2. "While that happens, keep doing other things."
3. "When the file is done, call my callback function."
4. Callback runs with the file contents.

This is the heartbeat of all async JavaScript — timers, file reading, network requests, database queries. Every one of them uses callbacks (or promises and async/await, which are built on the same concept).


3. Passing Functions as Arguments

Now let us look at this pattern more carefully with a variety of examples, starting simple and building up.

Example 1 — A function that runs another function

function runTwice(fn) {
  fn();
  fn();
}

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

runTwice(sayHello);
// Output:
// Hello!
// Hello!

runTwice does not know or care what fn does. It just calls it twice. You control the behaviour by choosing which function to pass in.

Example 2 — Passing a function with arguments

function applyToNumber(num, operation) {
  return operation(num);
}

function double(n) {
  return n * 2;
}

function square(n) {
  return n * n;
}

console.log(applyToNumber(5, double)); // 10
console.log(applyToNumber(5, square)); // 25

The same applyToNumber function behaves differently depending on what callback you pass. This is flexible, reusable design.

Example 3 — Using an anonymous arrow function

function applyToNumber(num, operation) {
  return operation(num);
}

console.log(applyToNumber(6, n => n + 10)); // 16
console.log(applyToNumber(6, n => n * n));  // 36
console.log(applyToNumber(6, n => n / 2));  // 3

Arrow functions make passing callbacks very concise — this is exactly how map(), filter(), and forEach() use callbacks.

Example 4 — Callback with multiple arguments

function calculate(a, b, operation) {
  const result = operation(a, b);
  console.log("Result:", result);
}

calculate(10, 3, function(x, y) { return x + y; }); // Result: 13
calculate(10, 3, function(x, y) { return x * y; }); // Result: 30
calculate(10, 3, function(x, y) { return x - y; }); // Result: 7

Passing a named vs anonymous function — what is the difference?

// Named function — defined separately
function showResult(value) {
  console.log("The result is:", value);
}
processData(42, showResult); // pass by name, no ()

// Anonymous function — defined inline
processData(42, function(value) {
  console.log("The result is:", value);
});

// Arrow function — shortest form
processData(42, value => console.log("The result is:", value));

All three do the same thing. Use named functions when you need to reuse the callback in multiple places. Use anonymous or arrow functions for one-off callbacks.

🔑 Remember: When passing a function as a callback, never add (). Parentheses mean "call this now." No parentheses means "here is the function, call it later."

// ✅ Correct — passing the function itself
setTimeout(greet, 1000);

// ❌ Wrong — calling greet immediately and passing its return value
setTimeout(greet(), 1000);

4. Callback Usage in Common Scenarios

Callbacks appear throughout everyday JavaScript. Here are the most common places you will encounter them.

Scenario 1 — setTimeout and setInterval

// Run a function once after a delay
setTimeout(function() {
  console.log("This runs after 2 seconds.");
}, 2000);

// Run a function repeatedly
const timer = setInterval(function() {
  console.log("This runs every 1 second.");
}, 1000);

// Stop the interval after 5 seconds
setTimeout(function() {
  clearInterval(timer);
  console.log("Timer stopped.");
}, 5000);

setTimeout and setInterval are entirely callback-based. You tell them "what to do" by passing a function.

Scenario 2 — Event listeners

const button = document.getElementById("myButton");

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

The function passed to addEventListener is a callback. It does not run now — it runs every time the user clicks the button. The browser manages when to call it.

// Named callback version — reusable
function handleClick() {
  console.log("Button was clicked!");
}

button.addEventListener("click", handleClick);

Scenario 3 — Array methods (map, filter, forEach)

Every array method you learned earlier uses callbacks:

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

// forEach — callback runs for each element
numbers.forEach(function(num) {
  console.log(num * 10);
}); // 10, 20, 30, 40, 50

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

// filter — callback decides what to keep
const evens = numbers.filter(function(num) {
  return num % 2 === 0;
});
console.log(evens); // [2, 4]

You have been using callbacks all along — map, filter, and forEach all accept a callback function as their argument.

Scenario 4 — File reading in Node.js

const fs = require("fs");

fs.readFile("message.txt", "utf8", function(error, content) {
  if (error) {
    console.log("Error:", error);
    return;
  }
  console.log("Message:", content);
});

The third argument to fs.readFile is a callback. Node.js calls it when the file read is complete, passing any error as the first argument and the file contents as the second.

Scenario 5 — Custom async simulation

You can write your own async-style functions using callbacks:

function fetchUserData(userId, onSuccess, onError) {
  setTimeout(function() {
    if (userId > 0) {
      const user = { id: userId, name: "Ananya", role: "admin" };
      onSuccess(user);
    } else {
      onError("Invalid user ID.");
    }
  }, 1000); // simulate network delay
}

fetchUserData(
  5,
  function(user) {
    console.log("Got user:", user.name); // "Got user: Ananya"
  },
  function(error) {
    console.log("Failed:", error);
  }
);

This pattern — separate callbacks for success and error — was very common before promises arrived.


5. The Basic Problem of Callback Nesting

Callbacks work perfectly for a single async operation. The trouble begins when you need to chain multiple async operations — where each step depends on the result of the previous one.

A realistic workflow

Imagine this sequence:

  1. Read a config file to get a database URL

  2. Use that URL to connect to the database

  3. Query the database for a user

  4. Fetch that user's orders

  5. Log the orders

With callbacks, each step must live inside the previous step's callback:

readConfigFile(function(error, config) {
  if (error) { return handleError(error); }

  connectToDatabase(config.dbUrl, function(error, db) {
    if (error) { return handleError(error); }

    db.findUser(userId, function(error, user) {
      if (error) { return handleError(error); }

      db.findOrders(user.id, function(error, orders) {
        if (error) { return handleError(error); }

        console.log("Orders:", orders);
        // And more steps would go deeper still...
      });
    });
  });
});

This structure is called Callback Hell — or the Pyramid of Doom, because of the triangular shape it forms as indentation keeps growing rightward.

The pyramid shape

doStep1(function(result1) {
    doStep2(result1, function(result2) {
        doStep3(result2, function(result3) {
            doStep4(result3, function(result4) {
                doStep5(result4, function(result5) {
                    // You are now very far to the right
                    console.log(result5);
                });
            });
        });
    });
});

Why this is a genuine problem

❌ Hard to read — logic is buried inside layers of indentation
❌ Hard to debug — an error anywhere means tracing through layers
❌ Error handling repeated at every level — easy to miss one
❌ Adding a new step means restructuring the whole block
❌ No way to run two steps in parallel
❌ Cannot easily reuse individual steps

Is there a solution?

Yes. Promises were invented specifically to solve callback hell. Instead of nesting callbacks, promises let you write the same multi-step logic in a flat, readable chain:

// The same 4-step workflow — with promises
readConfigFile()
  .then(config  => connectToDatabase(config.dbUrl))
  .then(db      => db.findUser(userId))
  .then(user    => db.findOrders(user.id))
  .then(orders  => console.log("Orders:", orders))
  .catch(error  => handleError(error)); // one handler for all errors

Same logic. Flat structure. One error handler. This is why promises quickly became the standard — and why async/await (built on promises) is what you will write in modern JavaScript.

Callback hell is not a reason to avoid callbacks — it is a reason to understand their limits and know when to reach for promises instead.


Diagrams

Function Calling Another Function (Callback Flow)

┌─────────────────────────────────────────────────────────────┐
│           FUNCTION CALLING ANOTHER FUNCTION                 │
│                                                             │
│  You define:                                                │
│  ┌───────────────┐     ┌─────────────────────┐             │
│  │  function     │     │  function           │             │
│  │  greet(name)  │     │  processUser(name,  │             │
│  │  {            │     │    callback) {      │             │
│  │    log(name)  │     │    ...              │             │
│  │  }            │     │    callback(name);  │             │
│  └───────────────┘     │  }                  │             │
│                        └─────────────────────┘             │
│                                                             │
│  You call:                                                  │
│  processUser("Priya", greet)                                │
│                        │                                    │
│                        ↓                                    │
│          processUser runs its own logic                     │
│                        │                                    │
│                        ↓                                    │
│          processUser calls callback("Priya")                │
│                        │                                    │
│                        ↓                                    │
│              greet("Priya") executes                        │
│              → "Hello, Priya!"                              │
│                                                             │
│  ✦ You control WHAT happens (by choosing which function)   │
│  ✦ processUser controls WHEN it happens                     │
└─────────────────────────────────────────────────────────────┘

Nested Callback Execution Flow (Callback Hell)

┌─────────────────────────────────────────────────────────────┐
│           NESTED CALLBACK EXECUTION FLOW                    │
│                                                             │
│  step1(function(result1) {       ← step 1 starts           │
│  │                                                          │
│  │   step2(result1, function(result2) {  ← step 2 starts   │
│  │   │                                                      │
│  │   │  step3(result2, function(result3) { ← step 3 starts │
│  │   │  │                                                   │
│  │   │  │  step4(result3, function(result4) {               │
│  │   │  │  │                                                │
│  │   │  │  │   console.log(result4);                        │
│  │   │  │  │                                                │
│  │   │  │  });  ← step 4 ends                              │
│  │   │  │                                                   │
│  │   │  });  ← step 3 ends                                 │
│  │   │                                                      │
│  │   });  ← step 2 ends                                    │
│  │                                                          │
│  });  ← step 1 ends                                        │
│                                                             │
│  Problems:                                                  │
│  ✦ Each step must wait inside the previous step's callback  │
│  ✦ Error handling needed at EVERY level                     │
│  ✦ Indentation grows → code drifts right → hard to read    │
│  ✦ Adding a step = restructuring everything                 │
│                                                             │
│  The fix → Promises (flat chain, one error handler)         │
└─────────────────────────────────────────────────────────────┘

Quick Reference

// ─── Basic callback ───────────────────────────────────────
function doSomething(value, callback) {
  const result = value * 2;
  callback(result);
}

doSomething(5, function(result) {
  console.log(result); // 10
});

// ─── Named vs anonymous callback ──────────────────────────
function handleResult(result) { console.log(result); }

doSomething(5, handleResult);                // named
doSomething(5, function(r) { log(r); });     // anonymous
doSomething(5, r => console.log(r));         // arrow function

// ─── setTimeout (async callback) ──────────────────────────
setTimeout(function() {
  console.log("Runs after 1 second");
}, 1000);

// ─── Array method callbacks ───────────────────────────────
[1, 2, 3].forEach(n => console.log(n));
[1, 2, 3].map(n => n * 2);
[1, 2, 3].filter(n => n > 1);

// ─── Node.js error-first callback pattern ─────────────────
fs.readFile("file.txt", "utf8", function(error, data) {
  if (error) { return console.log("Error:", error); }
  console.log("Data:", data);
});

// ─── Callback hell (avoid this) ───────────────────────────
step1(function(r1) {
  step2(r1, function(r2) {
    step3(r2, function(r3) {
      // deeply nested — use promises instead
    });
  });
});

Callback vs Promise — One Glance

Situation Callback Promise
Single async task ✅ Works great ✅ Works great
Multiple chained tasks ❌ Becomes nested ✅ Flat chain
Error handling Repeated at every level One .catch()
Readability Degrades with depth Stays readable
Modern code standard Older style Current standard

Wrapping Up

Callbacks are the original building block of async JavaScript — and they still appear everywhere in modern code. Here is what you learned:

  • A callback is a function passed into another function to be called later

  • Functions in JavaScript are values — they can be stored, passed, and returned

  • Callbacks power setTimeout, event listeners, map(), filter(), forEach(), file reading, and much more

  • The error-first callback pattern is the convention in Node.js — function(error, result)

  • Callback hell happens when you nest too many async callbacks — the code becomes unreadable and hard to maintain

  • Promises were built specifically to solve callback hell

Understanding callbacks deeply makes everything else in JavaScript async programming easier — promises, async/await, and event-driven architecture all build on the same idea of "pass a function, call it when ready."

Happy coding! 🚀


Try the examples in your browser console. The best way to feel comfortable with callbacks is to write your own function that accepts and calls another function — start there.