Skip to main content

Command Palette

Search for a command to run...

Callback Functions

Passing Functions Around Like Notes In Class

Published
6 min read
Callback Functions

Before callbacks make sense, one thing has to click: in JavaScript, a function is a value. Like a number. Like a string. You can store it in a variable, pass it to another function, even return it from one. Most people know this in theory and then forget it the moment they try to use it.

Here's what that looks like in practice:

// Storing a function in a variable 
const greet = function(name) {
  console.log("Hello, " + name);
};

// Passing that variable to another function
function run(fn) {
  fn("Aditya"); // calls whatever was passed in
}

run(greet); // prints: Hello, Aditya

greet is just a value here. We handed it to run, and run called it. That's the whole mechanic. A callback is just a function passed to another function so that other function can call it later.

The word "callback" sounds formal, but the idea is simple: you're giving someone your phone number and saying "call me back when you're done." The function you pass is that phone number.

Passing Functions as Arguments

You've probably used callbacks already without noticing. addEventListener, setTimeout, Array.forEach, all of these take a function as an argument. That function is the callback.

// setTimeout takes a callback: runs it after 2 seconds
setTimeout(function() {
  console.log("Two seconds later");
}, 2000);

// forEach takes a callback: runs it once per item
["Manas", "Saumya", "Priya"].forEach(function(name) {
  console.log(name);
});

// addEventListener takes a callback: runs it on click
button.addEventListener("click", function() {
  console.log("Clicked");
});

Notice the pattern. You pass a function, something else holds onto it, and calls it at the right moment. You are not in charge of when it runs. That's kind of the point.

Why Async Even Needs Callbacks

JavaScript runs one thing at a time. There is no threading, no parallel execution of your code. One call stack, one line at a time. So when you need to wait for something, a network request, a file read, a timer, you have a problem: if you wait, everything else stops.

Think about a restaurant. The chef doesn't stand at the kitchen pass staring at your food until you come collect it. They put it on the counter, ring a bell, and move on to the next order. You come when you're ready. The kitchen keeps running.

Callbacks are that bell. You hand the runtime a function and say: when the thing is done, run this. Meanwhile, the rest of your code keeps going.

// SYNCHRONOUS: second log waits for the whole loop
console.log("start");
for (let i = 0; i < 1000000000; i++) {} // browser hangs here
console.log("end");

// ASYNC WITH CALLBACK: second log runs immediately
console.log("start");
setTimeout(function() {
  console.log("I ran later"); // runs after 2s, non-blocking
}, 2000);
console.log("end"); // this prints before the callback

Running that second block, you'll see "start", then "end", then two seconds later "I ran later". The callback didn't block anything.

Common Places Callbacks Show Up

Callbacks are everywhere. Once you start seeing the pattern, you can't unsee it. Here are the places you'll hit them most as a beginner.

// array methods: forEach, filter, map
const scores = [42, 88, 61, 95, 37];

// forEach: run callback for each item
scores.forEach(function(score) {
  console.log(score);
});

// filter: keep items where callback returns true
const passing = scores.filter(function(score) {
  return score >= 60;
}); // [88, 61, 95]

// map: transform each item, return new array
const grades = scores.map(function(score) {
  return score >= 60 ? "Pass" : "Fail";
}); // ["Fail", "Pass", "Pass", "Pass", "Fail"]
// Event Listeners
const btn = document.getElementById("submit");

// callback runs every time the button is clicked
btn.addEventListener("click", function(event) {
  event.preventDefault();
  console.log("Form submitted");
});
// fetch API
// fetch uses .then()  which takes a callback
fetch("/api/users/1")
  .then(function(response) {
    return response.json(); // callback 1: parse response
  })
  .then(function(data) {
    console.log(data.name); // callback 2: use the data
  });

Arrow functions are common shorthand for callbacks: scores.filter(s => s >= 60) does the same thing as the function version above. Same idea, fewer keystrokes.

Writing Your Own Function That Takes a Callback

It helps to write one yourself, even if it's small. Once you've built a function that accepts and calls a callback, the whole model locks in.

// greetUser takes a name and a callback
function greetUser(name, callback) {
  console.log("Fetching user data...");
  callback(name); // call whatever was passed in
}

// Two different behaviours, same greetUser function
greetUser("Saumya", function(name) {
  console.log("Hello, " + name + "!"); // "Hello, Saumya!"
});

greetUser("Manas", function(name) {
  console.log("Welcome back, " + name); // "Welcome back, Manas"
});

greetUser doesn't care what the callback does. It just calls it with the name. The caller decides the behaviour. That flexibility is exactly why callbacks are so useful — you write the structure once, plug in different logic as needed.

A real-world version of this is Array.sort. The sort algorithm is built into JavaScript, but you pass a callback that defines how to compare two items. You bring the comparison logic; sort brings the algorithm.

// sort with custom comparator callback
const users = [
  { name: "Manas",  age: 28 },
  { name: "Saumya", age: 24 },
  { name: "Priya",  age: 31 },
];

// sort needs to know HOW to compare — you provide that
users.sort(function(a, b) {
  return a.age - b.age; // youngest first
});

The Nesting Problem

Here's where callbacks get genuinely painful. Suppose you need to do three async things in sequence: fetch a user, then fetch their orders, then fetch the details of the first order. Each step depends on the previous one. With callbacks, you end up putting them inside each other.

getUser(1, function(user) {

  getOrders(user.id, function(orders) {

    getOrderDetails(orders[0].id, function(details) {

      console.log(details); // finally, the thing we wanted

    });
  });
});

Three levels. Now add error handling at each step. Add a fourth request. The indentation becomes a visual pyramid, and the logic is buried inside it. This has a name: callback hell.

The nesting itself is not wrong, it works. The problem is readability. Errors are hard to catch at the right level. Adding a step means nesting deeper. Debugging means tracing through a maze of closing braces. It gets old fast.

Callback hell is why Promises and async/await exist. They solve the same coordination problem with much flatter code. That's the next part of this series.

What Callbacks Actually Give You

REFERENCES:

Simply JavaScript

Part 2 of 25

JavaScript is a quirky language. To master it, one should know to avoid its hidden traps along with its logic. This series showcase my journey through JS: the pain points, the breakthroughs, and the coding standards that I adopted from my mentors.

Up next

The New Keyword

What Actually Happens When You Write new Person()