Function calling another function flow

To understand callbacks, you first have to accept one unusual truth about JavaScript: functions are just values.
Functions Are Values, The Foundation Everything Else Rests On
In most beginner-level thinking, a function is something you call. You write it, you invoke it, it runs. That's it.
But JavaScript goes further. A function in JavaScript is a first-class value, meaning it can be stored in a variable, placed in an array, attached to an object, and most importantly, passed to another function as an argument.
// A function stored in a variable
const greet = function(name) {
return `Hello, ${name}!`;
};
// A function stored in an array
const operations = [
function(a, b) { return a + b; },
function(a, b) { return a * b; }
];
// A function stored in an object
const calculator = {
add: function(a, b) { return a + b; },
multiply: function(a, b) { return a * b; }
};
None of these functions are being called yet. They're just sitting there as values, the same way a number or a string sits in a variable. You can hand them around, store them, and decide to run them later.
This single idea, functions as portable values, is what makes callbacks possible.
What Is a Callback Function?
A callback is a function that you pass into another function, with the expectation that the receiving function will call it back at the right moment.
Start with the simplest possible example:
function doSomething(callback) {
console.log("Doing something...");
callback(); // "call back" the function we received
}
function finished() {
console.log("All done!");
}
doSomething(finished);
// "Doing something..."
// "All done!"
Notice what happened: finished was passed into doSomething without parentheses. That's not an accident — finished (no parentheses) is the function value itself. finished() (with parentheses) would call it immediately and pass the return value instead.
doSomething(finished); // Passes the function — called at the right time
doSomething(finished()); // Calls finished immediately, passes undefined
This is the most common beginner mistake with callbacks. Keep it in mind.
A More Concrete Example
Here's a function that applies any operation to two numbers:
function calculate(a, b, operation) {
const result = operation(a, b);
console.log(`Result: ${result}`);
}
function add(a, b) { return a + b; }
function multiply(a, b) { return a * b; }
function subtract(a, b) { return a - b; }
calculate(10, 5, add); // Result: 15
calculate(10, 5, multiply); // Result: 50
calculate(10, 5, subtract); // Result: 5
calculate doesn't care what the operation does. It just calls whatever function it was handed. The behavior is completely customizable from the outside, without rewriting calculate each time. That flexibility is the whole point.
Functions You Already Know That Use Callbacks
Before you ever consciously wrote a callback, you were already using them. JavaScript's built-in array methods are built entirely around the callback pattern:
const numbers = [1, 2, 3, 4, 5];
// forEach — callback called once per element
numbers.forEach(function(num) {
console.log(num * 2);
});
// 2, 4, 6, 8, 10
// filter — callback decides what stays
const evens = numbers.filter(function(num) {
return num % 2 === 0;
});
// [2, 4]
// map — callback transforms each element
const squared = numbers.map(function(num) {
return num * num;
});
// [1, 4, 9, 16, 25]
// sort — callback defines the sort order
const names = ["Priya", "Arjun", "Zara", "Mihir"];
names.sort(function(a, b) {
return a.localeCompare(b);
});
// ["Arjun", "Mihir", "Priya", "Zara"]
Every time you pass a function to forEach, map, filter, or sort, that function is a callback. The array method decides when to call it, once per item, or twice during a comparison — and you decide what it does.
Why Callbacks Exist: The Asynchronous Problem
Synchronous code runs line by line, in order, and nothing moves forward until the current line finishes. That works fine for logic. It fails for the real world.
Consider what "waiting" looks like in a program:
// Imagine this takes 3 seconds
const data = fetchDataFromServer();
// This line can't run until fetchDataFromServer is done
console.log(data);
If JavaScript worked this way, your entire browser would freeze for three seconds. No scrolling, no typing, no animations, just a locked screen while the network request completes. That's unacceptable.
JavaScript solves this with an asynchronous model: instead of waiting, you say "start this task, and when it's done, call this function with the result." That function you hand over is the callback.
Synchronous (blocking):
─────────────────────────────────────────────────────▶
[Start task] ──── wait ──── wait ──── wait ──── [Done] [Next line runs]
Asynchronous with callback (non-blocking):
─────────────────────────────────────────────────────▶
[Start task] ─────────────────────────────────── [Callback runs when ready]
[Next line runs immediately] ─────────────────▶
Callbacks in Common Scenarios
Scenario 1: setTimeout — Delayed Execution
The simplest async callback: run a function after a delay.
console.log("Starting timer...");
setTimeout(function() {
console.log("3 seconds later!");
}, 3000);
console.log("This runs immediately — before the timer fires.");
// Output order:
// "Starting timer..."
// "This runs immediately — before the timer fires."
// (3 seconds pass)
// "3 seconds later!"
setTimeout hands off the waiting to the browser. JavaScript doesn't sit idle, it moves on. When the timer expires, the browser places the callback into the queue to run.
Scenario 2: Event Listeners — User Interaction
Every browser event is a callback waiting for a trigger:
const button = document.querySelector('#submit-btn');
button.addEventListener('click', function(event) {
console.log(`Button clicked at: ${event.timeStamp}`);
console.log("Processing form...");
});
// This callback sits quietly and does nothing
// until the user actually clicks the button
The callback isn't called once and forgotten, it's registered and called every time the event fires. The event system is entirely built on the callback pattern.
Scenario 3: Reading Files (Node.js)
On the server side, file system operations use callbacks so the server can keep handling requests while a file loads:
const fs = require('fs');
console.log("Reading file...");
fs.readFile('data.json', 'utf8', function(error, contents) {
if (error) {
console.error(`Failed to read file: ${error.message}`);
return;
}
console.log("File contents:", contents);
});
console.log("Server is still running while file loads...");
// Output:
// "Reading file..."
// "Server is still running while file loads..."
// (file finishes loading)
// "File contents: ..."
Notice the callback receives two arguments: error and contents. This is the Node.js error-first callback convention, the first argument is always an error (or null if everything succeeded), and the rest are the results. It's a consistent pattern across Node's core APIs.
Scenario 4: Simulating an API Call
Here's a pattern that simulates what real async data fetching looks like using callbacks:
function fetchUser(userId, onSuccess, onError) {
console.log(`Fetching user ${userId}...`);
// Simulate network delay
setTimeout(function() {
if (userId <= 0) {
onError(new Error("Invalid user ID"));
return;
}
const user = { id: userId, name: "Arjun Sharma", role: "developer" };
onSuccess(user);
}, 1000);
}
fetchUser(
42,
function(user) {
console.log(`Got user: \({user.name} (\){user.role})`);
},
function(err) {
console.error(`Error: ${err.message}`);
}
);
// "Fetching user 42..."
// (1 second later)
// "Got user: Arjun Sharma (developer)"
Two callbacks, one for success, one for failure. The calling code stays clean; the function decides which one fires.
The Problem: Callback Nesting
So far, callbacks are elegant. One async task, one callback, perfectly readable.
The trouble starts when async tasks depend on each other. You need the result of the first before you can start the second, and the result of the second before you can start the third. Each task needs its own callback. And callbacks start nesting inside each other.
Consider a realistic flow: fetch a user, then fetch their orders, then fetch the details of the first order:
fetchUser(userId, function(user) {
// Got the user — now fetch their orders
fetchOrders(user.id, function(orders) {
// Got orders — now fetch the first order's details
fetchOrderDetails(orders[0].id, function(details) {
// Got details — now display it
displayOrderSummary(user, orders, details, function() {
// Displayed — now log the action
logActivity(user.id, 'viewed_order', function() {
console.log("Done.");
// What if we need to do one more thing?
});
});
});
});
}, function(error) {
console.error("Failed:", error.message);
});
Each step pushes inward. The code forms a shape developers have a name for:
fetchUser( ← level 1
fetchOrders( ← level 2
fetchOrderDetails(← level 3
displaySummary( ← level 4
logActivity( ← level 5
... ← level 6
This is Callback Hell, sometimes called the Pyramid of Doom after the visual shape it creates. It's not just ugly. It creates real problems:
Error handling becomes duplicated and fragile. Each callback needs its own error check. Miss one and an error silently disappears into the void.
fetchUser(userId, function(user) {
fetchOrders(user.id, function(orders) {
fetchOrderDetails(orders[0].id, function(details) {
// Three levels deep — where does an error from level 1 go?
// What if fetchOrders also fails? Separate handler?
}, handleError);
}, handleError);
}, handleError);
The flow of logic is invisible. To understand what happens in what order, you have to trace the nesting inward, step by step, rather than reading top to bottom.
Reuse becomes nearly impossible. The inner callbacks are buried. You can't easily pull fetchOrderDetails out and use it somewhere else without restructuring the entire pyramid.
Debugging is painful. Stack traces point deep into nested anonymous functions. It's hard to tell which callback fired when, or which level an error originated from.
Here's the conceptual comparison:
What you want to say:
1. Fetch the user
2. Then fetch their orders
3. Then fetch the first order's details
4. Then display everything
What deeply nested callbacks look like to read:
Start here (step 1) → go deeper → (step 2) → deeper → (step 3)
→ deepest → (step 4) → unwind → unwind → unwind
The logical sequence — 1, 2, 3, 4 — is perfectly linear. But the code structure isn't linear at all. It spirals inward, and the deeper you go, the harder it becomes to follow.
This conceptual mismatch between "how we think about steps in a sequence" and "how deeply nested callbacks are structured" is the core reason JavaScript eventually needed something better.
What Comes Next
Callbacks aren't wrong, they're the correct mental model for asynchronous programming. The idea of "start this task, and call me back when it's done" is exactly right. What becomes problematic is nesting callbacks inside callbacks inside callbacks, which is a code organization problem, not a conceptual one.
The JavaScript ecosystem eventually introduced Promises to express async sequences without nesting, and later async/await to write them as if they were synchronous — all built on the same foundation that callbacks established.
But understanding why callbacks exist, and what problem they were solving, makes everything that came after far easier to understand. Promises didn't replace callbacks — they tamed them.
Key Takeaways
A callback is a function passed as an argument to another function, to be called later.
Functions are first-class values in JavaScript — this is what makes callbacks possible.
Pass functions without parentheses to avoid calling them immediately.
Callbacks solve the asynchronous problem: JavaScript doesn't block while waiting for slow tasks.
You've used callbacks already —
forEach,map,filter,addEventListener,setTimeoutall use them.The error-first convention in Node.js puts the error as the first callback argument.
Callback Hell (Pyramid of Doom) happens when async tasks depend on each other and callbacks nest deeply.
The problems with deep nesting are: duplicated error handling, unreadable flow, poor reusability, and painful debugging.
Promises and
async/awaitwere built to solve callback hell, but callbacks are the foundation they stand on.
Quick Reference
// Passing a function as a callback
doWork(myFunction); // Passes the function
doWork(myFunction()); // Calls it immediately, passes the return value
// Inline (anonymous) callback
doWork(function(result) {
console.log(result);
});
// Arrow function callback (modern shorthand)
doWork((result) => {
console.log(result);
});
// setTimeout — most basic async callback
setTimeout(function() { /* runs later */ }, 1000);
// Event listener callback
element.addEventListener('click', function(event) { /* runs on click */ });
// Node.js error-first callback convention
fs.readFile('file.txt', function(error, data) {
if (error) { /* handle error */ return; }
/* use data */
});
// Success + error callbacks (manual pattern)
fetchData(
function onSuccess(data) { /* handle result */ },
function onError(err) { /* handle failure */ }
);
Callbacks are JavaScript's handshake with time. Once you understand why they exist, the whole async model clicks into place.

