6.3 — Callbacks & callback hell
You now know async results arrive later — so how does later-code get the value? The original answer, powering a decade of JavaScript: pass a callback — "when you're done, call this function with the result." It's 3.8's higher-order functions, doing the heaviest lifting of their careers.
It works. And then real life asks for step after step — load the user, then their cart, then pay — and the pattern's flaw appears: each next step must nest inside the previous callback, and the code slides rightward into the pyramid of doom. Today you build the pyramid with your own hands and feel exactly why promises had to be invented.
function loadUser(cb) {
setTimeout(() => cb("vas"), 40);
}
function loadCart(user, cb) {
setTimeout(() => cb(user + "'s cart"), 40);
}
function pay(cart, cb) {
setTimeout(() => cb(cart + " paid"), 40);
}
loadUser((user) => {
loadCart(user, (cart) => {
pay(cart, (receipt) => {
console.log(receipt);
});
});
});The three helpers all follow the callback contract: do async work (here a small timer stands in for the network), and when done, call cb(result).
The deeper story, with the real names for things — this part is what turns “I saw it” into “I can explain it.”
The pattern's proper name is continuation-passing. Instead of returning a value, a function receives "the rest of the program" (the continuation) as an argument. It calls it when ready. Every callback API you'll ever meet — timers, old Node file APIs, event listeners in Phase 7 — is this one idea.
Be precise about what's broken. ONE level of callback is completely fine and stays common everywhere — every addEventListener, every test('…', fn) in your Phase 10 future is a callback. The failure mode is sequencing chains of dependent async steps — that's where nesting compounds, errors scatter, and reading order stops matching running order.
The cure's shape is worth previewing precisely. Promises turn "hand your next step INTO the function" into "the function HANDS BACK an object you can attach next steps to." Return-a-thing beats take-a-thing — it flattens the pyramid into a chain and gives errors one shared drain. That object is lesson 6.4.
⌨️ a two-step relay, callback style
Feel the pattern (and its shape) in your own hands: two async steps where the second NEEDS the first’s result — so it must live inside the first’s callback.
requirements:
- A function
fetchName(cb)that waits 30ms (setTimeout), then callscb("vasavi"). - A function
makeBadge(name, cb)that waits 30ms, then callscb("badge: " + name). - Wire the relay: fetch the name, and INSIDE its callback, make the badge from it, and inside THAT callback print the result. One print, two levels deep — feel the slide.
when you press RUN, the console must show exactly:
✏️ Quick check 1
Where does the value "vas" exist? Type: inside or outside (the callback).
function loadUser(cb) {
setTimeout(() => cb("vas"), 40);
}
loadUser((user) => {
// here?
});
// or here?✏️ Quick check 2
Step B needs step A’s result. In callback style, where must B’s call be written? Type: inside or after (A’s callback).
✏️ Quick check 3
Can a try/catch wrapped around loadUser(…) catch an error thrown LATER inside its callback? Type yes or no.
🗣️ Now teach it back
Explain to a friend how async code gets its results using callbacks, why chaining dependent steps creates the pyramid of doom, and why error handling makes it even worse.
Write it as if your friend is sitting next to you. Saved to your journal — future-you will use these notes to teach others.