Callbacks in JavaScript: Why They Exist
Callback - "Don't call us, we'll call you."

The Flow of CallBack
To understand callbacks, first, we have to know about functions.
Functions are non-primitive data types in JavaScript. But, functions are First-Class Citizens. This means a function is just another piece of data, like a string or a number. You can:
Store a function in a variable.
Pass a function as an argument to another function.
Return a function from another function.
Store a function in a variable
// 1. Store a function in a variable (The Recipe)
const makePasta = function() {
console.log("Cooking delicious pasta... 🍝");
};
makePasta()
Output:
"Cooking delicious pasta... 🍝"
We store the function in a variable when we want to execute the function after declaration. In Js there is a concept of Hoisting. Function hoist the whole body and variable hoists the reference. To know more read the MDN docs.
Pass a function as an argument to another function
// 2. Pass a function as an argument (The Sous-Chef)
// 'callback' is the recipe we hand to the chef
function chef(customerName, callback) {
console.log("Chef is preparing the order for " + customerName);
callback(); // Calling the function we passed in
}
// CALLING IT: We pass the 'makePasta' recipe to the chef
chef("Alice", makePasta);
Output:
Chef is preparing the order for Alice
Cooking delicious pasta... 🍝
The Execution Flow
Definition Phase: The browser reads your code and remembers two things:
There is a "recipe" called
makePasta.There is a "worker" called
chefwho expects a name and a set of instructions.
The Call (
chef("Alice", makePasta)):The program jumps into the
cheffunction.It assigns
"Alice"to the variablecustomerName.It assigns the entire
makePastafunction to the variablecallback. (Crucial: It hasn't "cooked" yet; it's just holding the recipe).
Inside the Chef:
Line 1:
console.logruns immediately. Output: "Chef is preparing the order for Alice".Line 2 (
callback()): The Chef looks at the recipe paper (callback) and sees the()"action" symbols. This triggers the code insidemakePasta.
The Callback Execution:
The program jumps out of the chef’s main logic and into the
makePastablock.Output: "Cooking delicious pasta... 🍝".
Completion:
makePastafinishes.The
cheffunction finishes.The program moves to the next line of code after the initial call
MAIN PROGRAM
|
|-- calls chef("Alice", makePasta)
|
|--> [Inside chef]
| 1. Logs "Chef is preparing..."
| 2. Sees callback() -------------> [Jumps to makePasta]
| 1. Logs "Cooking delicious..."
| 2. Finishes
| <------------------------------------ [Returns to chef]
|
|--> [chef finishes]
Return a function from another function
// 3. Return a function from another function
function getRecipeGenerator(style) {
if (style === "spicy") {
return function() { console.log("Adding chili flakes... 🌶️"); };
} else {
return function() { console.log("Adding extra cheese... 🧀"); };
}
}
// CALLING IT:
// First, we get the specific recipe (the returned function)
const mySecretRecipe = getRecipeGenerator("spicy");
// Then, we actually execute that recipe whenever we want
mySecretRecipe();
Output:
Adding chili flakes... 🌶️
The Execution Flow
Definition Phase:
- The browser stores the
getRecipeGeneratorlogic. It knows this function is a "Generator"—its only job is to hand back a smaller, specific function.
- The browser stores the
The First Call (
const mySecretRecipe = getRecipeGenerator("spicy")):The program enters
getRecipeGeneratorwith the input"spicy".It checks the
ifstatement: Is it spicy? Yes.It hits the
returnkeyword. Instead of returning a number or string, it returns the entire block of code inside thatfunction() { ... }.The variable
mySecretRecipenow is that function. It is sitting in your memory, waiting to be triggered.Note: Nothing has been logged to the console yet.
The Second Call (
mySecretRecipe()):This is the "Action" phase. You use the
()parentheses on your new variable.The program jumps into the code that was returned in Step 2.
Output: "Adding chili flakes... 🌶️".
The Completion:
The code inside the returned function finishes.
The program moves on.
STEP 1: The Request
[ Main Code ] --- "Give me a spicy recipe" ---> [ getRecipeGenerator ]
|
(Checks "spicy" logic)
|
[ Main Code ] <--- [ Returns Function Block ] <---------
STEP 2: The Storage
mySecretRecipe = function() { console.log("Adding chili flakes... 🌶️"); }
STEP 3: The Execution
mySecretRecipe() ---> [ Executes the stored block ]
Output: "Adding chili flakes... 🌶️"
What is a Callback?
In JavaScript, functions are First-Class Citizens. This means they can be treated like any other variable: you can assign them to values, return them from other functions, and—most importantly—pass them as arguments to other functions.
A callback is simply a function passed into another function with the expectation that it will be executed (called back) at a later time.
Why Callbacks for Async Programming?
JavaScript is single-threaded, meaning it can only do one thing at a time. If you ask it to fetch data from a server, the whole browser would freeze while waiting for the response.
Callbacks solve this by saying: "Go start this long task. I’m going to keep running other code. When you’re finished, run this specific callback function."
Common Scenarios:
Event Listeners:
button.addEventListener('click', () => { ... })Timers:
setTimeout(doSomething, 2000)Data Fetching: Handling the result of a database query or API call.
The Conceptual Flow
Function A is called with Function B (the callback) as an argument.
Function A starts an asynchronous task (like a timer).
The main program continues running.
The task finishes, and Function B is pushed onto the execution stack to run.
The Problem: Callback Hell
When you have multiple asynchronous tasks that depend on each other, you end up nesting callbacks inside callbacks. This creates a "pyramid" shape in your code that is incredibly hard to read and debug.
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
console.log(c); // This is "Callback Hell"
});
});
});
The biggest problem with callbacks is nesting. If you have three tasks that must happen one after the other (Step A 👉Step B👉Step C), your code starts to crawl sideways.
Conceptually, it looks like this:
Do Task 1...
...once Task 1 is done, do Task 2...
...once Task 2 is done, do Task 3...
- ...once Task 3 is done, finally finish.
This creates a "Pyramid of Doom." It’s hard to read, even harder to handle errors (where did it break?), and makes the code very brittle.
📝 Assignment: The "Custom Coffee" Challenge
To wrap up, try this exercise to see if you’ve mastered the flow of functions as values and callbacks.
The Scenario: You are building a Coffee Shop Simulator.
Step 1: Create a function called
makeCoffee. It should take two arguments:type(a string) andonDone(a callback function).Step 2: Inside
makeCoffee, log: "Brewing your [type]... Please wait."Step 3: Use
onDone()to signal the coffee is ready.Step 4: Define a separate function called
alertCustomer. It should log: "Your order is ready! ☕"Step 5 (The Execution): Call
makeCoffee, passing in "Latte" and thealertCustomerfunction.
Bonus Goal: Can you wrap onDone() in a setTimeout so the alert only happens after 3 seconds?
🏁 Conclusion
Callbacks might feel like "code inception" at first, but they are the secret sauce that makes JavaScript fast and interactive. By treating functions as values—things you can store, pass around, and return—you gain the power to:
Wait without freezing: Your app stays responsive while heavy tasks happen in the background.
Write cleaner logic: You can separate "what to do" (the recipe) from "when to do it" (the chef).
Create dynamic code: You can build "function factories" that generate custom logic on the fly.
Happy coding! 💕






