Callback Hell (also called Pyramid of Doom or Callback Spaghetti) happens when you write multiple nested callback functions to handle asynchronous operations in JavaScript.
As more async tasks depend on each other, your code becomes:
👎 Hard to read
👎 Hard to debug
👎 Hard to maintain
👎 Error-prone
📌 Why Callback Hell Happens
Callback hell usually occurs when:
1️⃣ Each async function depends on the previous result
Example:
Login → Fetch videos → Fetch video details.
2️⃣ You nest callbacks inside callbacks
This leads to a deep “pyramid” shape:
login(() => {
getVideos(() => {
getDetails(() => {
// ...
});
});
});3️⃣ No separation of concerns
All logic is squeezed inside nested functions.
📌 Callback Hell Example
console.log('Start');
function loginUser(email, password, callback) {
setTimeout(() => {
console.log('User Logged In');
callback({ userEmail: email });
}, 1000);
}
function getUserVideos(email, callback) {
setTimeout(() => {
console.log("Got Videos");
callback(['video1', 'video2', 'video3']);
}, 2000);
}
function videoDetails(video, callback) {
setTimeout(() => {
callback('title of the video');
}, 3000);
}
const user = loginUser('codeveda@goomail.com', 123456, (user) => {
console.log(user);
getUserVideos(user.userEmail, (videos) => {
console.log(videos);
videoDetails(videos[0], (title) => {
console.log(title);
});
});
});
console.log('Finish')Start
Finish
User Logged In
{ userEmail: 'codeveda@goomail.com' }
Got Videos
[ 'video1', 'video2', 'video3' ]
title of the video<div class="infographic-callbackhell">
<style>
.infographic-callbackhell {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
--bad: #c8452e;
--bad-soft: #f7e3df;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-callbackhell[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
--bad: #e0715a;
--bad-soft: #2e211d;
}
.infographic-callbackhell * { box-sizing: border-box; }
.infographic-callbackhell h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-callbackhell p { margin: 0; }
.infographic-callbackhell button { font-family: inherit; }
.ch-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.ch-hero { text-align: center; margin-bottom: 1.3rem; }
.ch-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.ch-hero p { color: var(--ink-soft); margin-top: 0.45rem; font-size: 0.85rem; }
/* the staircase */
.ch-pyramid { display: flex; flex-direction: column; gap: 0.35rem; margin-bottom: 1.4rem; }
.ch-level {
border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.45rem 0.65rem;
font-family: 'IBM Plex Mono', monospace; font-size: 0.68rem; color: var(--ink-soft);
opacity: 0.3; transform: translateX(-6px);
transition: opacity 0.4s ease, transform 0.4s ease, border-color 0.4s ease, background 0.4s ease, color 0.4s ease;
}
.ch-level.is-active { opacity: 1; transform: translateX(0); border-color: var(--accent); background: var(--accent-soft); color: var(--ink); font-weight: 600; }
.ch-level[data-depth="0"] { margin-left: 0; }
.ch-level[data-depth="1"] { margin-left: 1.1rem; }
.ch-level[data-depth="2"] { margin-left: 2.2rem; }
.ch-level[data-depth="3"] { margin-left: 3.3rem; }
.ch-timer-tag { display: inline-block; font-size: 0.6rem; padding: 0.05rem 0.35rem; border: 1px solid var(--line); color: var(--ink-soft); margin-left: 0.4rem; border-radius: 3px; }
.ch-level.is-active .ch-timer-tag { border-color: var(--accent); color: var(--accent); }
.ch-controls { display: flex; align-items: center; justify-content: center; gap: 0.8rem; margin-top: 1rem; }
.ch-nav { border: 1px solid var(--line); background: var(--paper-alt); color: var(--ink); padding: 0.4rem 0.9rem; font-size: 0.78rem; font-weight: 600; cursor: pointer; }
.ch-nav:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.ch-nav:disabled { opacity: 0.35; cursor: default; }
.ch-step-count { font-family: 'IBM Plex Mono', monospace; font-size: 0.72rem; color: var(--ink-soft); min-width: 78px; text-align: center; }
.ch-run { display: block; margin: 1rem auto 0; border: none; background: var(--accent); color: #fff; padding: 0.5rem 1.2rem; }
.ch-run:hover:not(:disabled) { color: #fff; }
.ch-run:active:not(:disabled) { transform: scale(0.96); }
.ch-caption-row { display: flex; justify-content: center; width: 100%; padding: 1rem }
.ch-caption { text-align: center; font-size: 0.84rem; color: var(--ink-soft); margin: 1.1rem 0 0; min-height: 2.6em; width: fit-content; }
.ch-caption strong { color: var(--ink); }
.ch-console {
margin: 1rem auto 0; max-width: 380px; text-align: left;
border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.55rem 0.8rem;
font-family: 'IBM Plex Mono', monospace; font-size: 0.7rem; min-height: 7.5em;
}
.ch-console .ch-clabel { font-size: 0.6rem; color: var(--ink-soft); margin-bottom: 0.25rem; display: flex; justify-content: space-between; }
.ch-console .ch-elapsed { color: var(--ink-soft); }
.ch-console .ch-line { opacity: 0; transition: opacity 0.3s ease; color: var(--good); }
.ch-console .ch-line.is-shown { opacity: 1; }
.ch-note { text-align: center; font-size: 0.8rem; color: var(--ink-soft); margin-top: 1.6rem; padding-top: 1.1rem; border-top: 1px dashed var(--line); }
.ch-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-callbackhell *, .infographic-callbackhell *::before, .infographic-callbackhell *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="ch-wrap">
<div class="ch-hero">
<h1>Callback hell</h1>
<p>Each step only knows how to start once the one before it finishes — so the code just keeps stepping right.</p>
</div>
<div class="ch-pyramid">
<div class="ch-level" data-depth="0" id="lvl0">loginUser(email, pass, (user) => { <span class="ch-timer-tag">1s</span></div>
<div class="ch-level" data-depth="1" id="lvl1">getUserVideos(user.email, (videos) => { <span class="ch-timer-tag">2s</span></div>
<div class="ch-level" data-depth="2" id="lvl2">videoDetails(videos[0], (title) => { <span class="ch-timer-tag">3s</span></div>
<div class="ch-level" data-depth="3" id="lvl3">console.log(title); }); }); });</div>
</div>
<div class="ch-console">
<div class="ch-clabel"><span>console</span><span class="ch-elapsed" id="chElapsed">t = 0s</span></div>
<div class="ch-line" id="cLineStart">Start</div>
<div class="ch-line" id="cLineFinish">Finish</div>
<div class="ch-line" id="cLineLoggedIn">User Logged In</div>
<div class="ch-line" id="cLineUser">{ userEmail: "codeveda@goomail.com" }</div>
<div class="ch-line" id="cLineGotVideos">Got Videos</div>
<div class="ch-line" id="cLineVideos">[ "video1", "video2", "video3" ]</div>
<div class="ch-line" id="cLineTitle">"title of the video"</div>
</div>
<button class="ch-nav ch-run" id="chRun" type="button">Run</button>
<div class="ch-caption-row">
<p class="ch-caption" id="chCaption">Click Run to trace the whole chain.</p>
</div>
<p class="ch-note">Nothing here is actually blocked — each timer runs in the background. But since <strong>step 2</strong> can't even start until <strong>step 1</strong>'s callback fires, the total time still adds up: <strong>1s + 2s + 3s = 6s</strong>, four levels deep.</p>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-callbackhell')
|| document.querySelector('.infographic-callbackhell');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') { dark = true; }
else if (bodyAttr === 'false') { dark = false; }
else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const steps = [
{
delay: 0, levels: [0], elapsed: 't = 0s',
logs: ['cLineStart', 'cLineFinish'],
caption: '<strong>Start</strong> and <strong>Finish</strong> print immediately. <strong>loginUser()</strong> only started a 1-second timer — it never blocked anything.'
},
{
delay: 700, levels: [0, 1], elapsed: 't = 1s',
logs: ['cLineStart', 'cLineFinish', 'cLineLoggedIn', 'cLineUser'],
caption: 'The login timer fires. Its callback runs, logs the user — and only now does <strong>getUserVideos()</strong> get called, one level deeper.'
},
{
delay: 1700, levels: [0, 1, 2], elapsed: 't = 3s',
logs: ['cLineStart', 'cLineFinish', 'cLineLoggedIn', 'cLineUser', 'cLineGotVideos', 'cLineVideos'],
caption: 'Two more seconds pass. Videos arrive — and <strong>videoDetails()</strong> starts, one level deeper still.'
},
{
delay: 3200, levels: [0, 1, 2, 3], elapsed: 't = 6s',
logs: ['cLineStart', 'cLineFinish', 'cLineLoggedIn', 'cLineUser', 'cLineGotVideos', 'cLineVideos', 'cLineTitle'],
caption: 'Three more seconds. The title finally prints — four callbacks deep, six seconds after the code started.'
},
{
delay: 3900, levels: [0, 1, 2, 3], elapsed: 't = 6s',
logs: ['cLineStart', 'cLineFinish', 'cLineLoggedIn', 'cLineUser', 'cLineGotVideos', 'cLineVideos', 'cLineTitle'],
caption: 'Every step waited on the one before it. That staircase shape, growing right with every dependency, is <strong>callback hell</strong>.'
},
];
const levelEls = [wrapper.querySelector('#lvl0'), wrapper.querySelector('#lvl1'), wrapper.querySelector('#lvl2'), wrapper.querySelector('#lvl3')];
const elapsedEl = wrapper.querySelector('#chElapsed');
const caption = wrapper.querySelector('#chCaption');
const runBtn = wrapper.querySelector('#chRun');
const allLogIds = ['cLineStart', 'cLineFinish', 'cLineLoggedIn', 'cLineUser', 'cLineGotVideos', 'cLineVideos', 'cLineTitle'];
function applyStep(s) {
levelEls.forEach((el, i) => el.classList.toggle('is-active', s.levels.includes(i)));
allLogIds.forEach(id => {
wrapper.querySelector('#' + id).classList.toggle('is-shown', s.logs.includes(id));
});
elapsedEl.textContent = s.elapsed;
caption.innerHTML = s.caption;
}
function reset() {
levelEls.forEach(el => el.classList.remove('is-active'));
allLogIds.forEach(id => wrapper.querySelector('#' + id).classList.remove('is-shown'));
elapsedEl.textContent = 't = 0s';
caption.textContent = 'Click Run to trace the whole chain.';
}
runBtn.addEventListener('click', () => {
reset();
runBtn.disabled = true;
steps.forEach(s => setTimeout(() => applyStep(s), s.delay));
const lastDelay = steps[steps.length - 1].delay;
setTimeout(() => { runBtn.disabled = false; }, lastDelay + 400);
});
})();
</script>🔍 Explanation of Flow
Startprints first → code begins executing.loginUser()runs → sets a 1-second timer → JS continues without waiting.Finishprints → because async timers don’t block execution.After 1 second:
User Logged Inprints.The callback runs and prints the user object.
getUserVideos()starts a 2-second timer.
After 2 more seconds:
Got Videosprints.The videos array prints.
videoDetails()starts a 3-second timer.
After 3 more seconds:
The video title prints.
Because each next step depends on the previous, it creates deep nesting → callback hell.
⚠️ Problems with Callback Hell
Here are the major issues developers face:
❗ 1. Deeply nested structure
Hard to read and understand.
❗ 2. Harder debugging
Errors inside nested callbacks are harder to trace.
❗ 3. No clear error handling
Every callback must manually handle errors → messy.
❗ 4. No modularity
All logic lives inside one callback chain.
❗ 5. Difficult to reuse code
Callbacks lock logic into nested blocks.
📌 How Modern JavaScript Avoids Callback Hell
JavaScript solved this problem using:
1. Promises
Flat .then() chains
Better error handling (.catch())
2. async / await (most used today)
Looks synchronous
No nesting
Cleaner, readable
3. Using named functions instead of anonymous ones
Helps reduce inline nesting