Understanding Callbacks in JavaScript
Before Promises and Async/Await, there was the callback. Here is how treating functions as values unlocks asynchronous programming.
Search for a command to run...
Before Promises and Async/Await, there was the callback. Here is how treating functions as values unlocks asynchronous programming.
No comments yet. Be the first to comment.
These are the assignments or knowledge dumps I decide to write for the "Chai aur Code's web Dev Cohort 2026"
Callbacks gave us the power to handle time. Promises gave us our sanity back. Here is how JavaScript manages future values.
RAG helps LLMs answer questions using your own data, but it doesn't guarantee correct answers. Here's how the pipeline works and where it commonly breaks down.

Imagine asking an AI, "Write a blog," versus "Act as an SEO expert and write a 1,000-word blog on AI safety formatted in markdown." The difference in the resulting output is night and day. A prompt is

Spotify and Apple Music make it incredibly easy to showcase your live listening habits on your website. YouTube Music? Not so much. After doing some setup you can do an API call and fetch your account

Arthur C. Clarke's famous third law: "Any sufficiently advanced technology is indistinguishable from magic" perfectly captures how the pace of human innovation works. From carrying supercomputers in o

Why HTTP is amnesiac, the difference between transport and state, and how to choose the right auth strategy for your Node.js app.
If you want to understand how JavaScript actually works, you have to accept one fundamental premise: in JavaScript, a function is just a value.
It is not a special, rigid structure. It is a piece of data. Just like you can assign a string to a variable, push a number into an array, or pass a boolean into a function, you can do all of those exact same things with a function.
Once you wrap your head around that, the concept of a callback stops being a confusing piece of jargon and becomes an incredibly obvious tool.
A callback is exactly what it sounds like: a function that you pass into another function as an argument, with the expectation that it will be called (executed) at a later time. Today, I want to look at why this pattern exists, how it powers the web, and why it ultimately forced JavaScript to evolve.
Let’s start synchronously. We won't worry about network requests or timers yet. Let’s just look at the raw mechanic of passing a function into another function.
Imagine we are writing a function that processes an array of numbers. We want it to be flexible—maybe we want to double the numbers, maybe we want to square them. Instead of hardcoding the math inside the processor, we can let the caller define the math by passing in a callback function.
// This is our callback function. It just knows how to double a number.
function double(n) {
return n * 2;
}
// This function takes an array, and a callback function to apply to each item.
function processNumbers(numbers, mathCallback) {
const result =[];
for (let i = 0; i < numbers.length; i++) {
// We execute the callback right here, passing the current number
result.push(mathCallback(numbers[i]));
}
return result;
}
const myNumbers = [1, 2, 3];
const doubledNumbers = processNumbers(myNumbers, double);
console.log(doubledNumbers); // [2, 4, 6]
Mental Diagram: The Synchronous Flow
processNumbers starts running.
It hits the loop and calls mathCallback (which is pointing to our double function).
The execution jumps to double, does the math, and returns the value.
processNumbers catches the value, pushes it to the array, and continues the loop.
Notice that we passed double into processNumbers without parentheses. We didn’t write processNumbers(myNumbers, double()). If we added parentheses, we would be passing the result of the function. By omitting them, we pass the function itself.
If this looks familiar, it’s because this is exactly how JavaScript's built-in array methods work. When you write [1, 2, 3].map(n => n * 2), you are creating an anonymous arrow function and passing it to .map() as a callback.
Synchronous callbacks are useful for code reuse, but they aren't the reason the callback pattern became the backbone of JavaScript. The real reason is asynchrony.
JavaScript is single-threaded. It has one call stack. It can only do one single thing at a time. If you tell JavaScript to fetch a massive file from a database, and it stops to wait for that file, the entire browser tab freezes. Users can't click buttons, animations stop, and the web page feels dead.
To prevent this, JavaScript offloads time-consuming tasks (like network requests or timers) to the browser. But JavaScript still needs a way to know when that task is finished.
Enter the callback. We hand the browser a task, and we say: "Hey, I’m going to keep running the rest of my code. When you finish downloading that file, call this function and pass the file into it."
The simplest example of this is setTimeout.
console.log("1. Starting the script");
setTimeout(() => {
console.log("2. The timer is done!");
}, 2000);
console.log("3. Continuing with other work...");
If you run this, the output is: 1. Starting the script 3. Continuing with other work... (two seconds pass) 2. The timer is done!
The arrow function passed into setTimeout is a callback. We handed it off, JavaScript moved on, and two seconds later, the event loop pushed our callback back onto the stack to be executed.
You write callbacks every day, often without thinking about the underlying architecture.
Event Listeners: When you click a button, you don't know when the user is going to click it. So you provide a callback.
const button = document.querySelector('#submit');
button.addEventListener('click', function(event) {
// This callback runs only when the click happens
console.log('Button clicked!');
});
Node.js File System: Before Promises were standardized, Node.js relied entirely on "error-first callbacks". You would ask the file system to read a file, and provide a function to handle the result or the error.
const fs = require('fs');
fs.readFile('user.json', 'utf8', (error, data) => {
if (error) {
console.error("Failed to read file:", error);
return;
}
console.log("File contents:", data);
});
Callbacks are simple and elegant for single, isolated tasks. But software is rarely isolated. Usually, one asynchronous action depends on the result of another.
Imagine a scenario where we need to:
Fetch a user from a database.
Using that user's ID, fetch their recent posts.
Using the ID of their first post, fetch the comments on that post.
If we use a traditional callback approach, the code ends up looking like this:
getUser('alice', function(userError, user) {
if (userError) return handleError(userError);
getPosts(user.id, function(postsError, posts) {
if (postsError) return handleError(postsError);
getComments(posts[0].id, function(commentsError, comments) {
if (commentsError) return handleError(commentsError);
console.log("Finally got the comments:", comments);
});
});
});
Mental Diagram: Nested Execution (Callback Hell)
getUser fires... waits... ↳ getPosts fires... waits... ↳ getComments fires... waits... ↳ Execute final logic.This triangular shape of code moving further and further to the right is notoriously known as Callback Hell or the Pyramid of Doom.
The problem here isn't just visual. The real issue is inversion of control. Every time you pass a callback to a function, you are giving away control of your program. You are trusting that getPosts will call your function exactly once, with the right arguments, and handle errors correctly. When you nest this deep, tracing the state of your application—or figuring out where a silent failure occurred—becomes an absolute nightmare.
The nesting problem was severe enough that it drove the JavaScript community to invent Promises, and eventually the async/await syntax we use today to make asynchronous code read like synchronous code.
But under the hood? It’s still callbacks. Promises.then() takes a callback. The event loop still operates by queueing up functions to be executed when the call stack is clear.
You don’t need to write deeply nested callbacks anymore, but understanding why they exist gives you x-ray vision into JavaScript. When you realize that functions are just data that can be carried around and triggered later, the entire event-driven architecture of the web suddenly makes perfect sense.