One track. One train. No overtaking.
👋 Hey folks!
In Part I and Part II, we discussed the Illusion: why browsers appear “instant” to Muggles (users) 🧑, and why that illusion breaks when we interact with the DOM.
Now, we strip away the magic and look at the foundation.
No Event Loop yet.
No microtasks.
No queues.
Only the thing that always happens first.
🚂 The Call Stack is a Train That Does Not Stop
Imagine the Hogwarts Express.
- It has one track (The Main Thread 🧵).
- It moves in one direction.
- While the train is on the track, nothing else can move.
This is your Call Stack.
JavaScript is synchronous by nature. It is single-minded. It executes spells (code) step-by-step and never gives up control halfway through.
The browser? Sure, it’s asynchronous. It has networking, timers, the rendering engine, the GPU… But JavaScript itself? It sits comfortably in the Call Stack, blocking the view.
And while the Call Stack is busy, the track is closed. ⛔️
🧪 Scenario 1: The Sync Trap (Click vs. .click())
Let’s look at a spell that confuses many wizards. We have a button, a listener, and some logs.
console.log("A: script start");
button.addEventListener("click", () => {
console.log("B: click start");
// 🪄 Synchronous array magic
const items = [1, 2, 3];
items.map((n) => {
console.log("C: map:", n);
return n * 2;
});
button.classList.add("active");
console.log("D: class added");
// 🦉 Future work (Async)
Promise.resolve().then(() => {
console.log("E: Promise.then");
});
setTimeout(() => {
console.log("F: setTimeout");
}, 0);
console.log("G: click end");
});
console.log("H: before click");
// ⚠️ Crucial moment: We trigger the click SYNCHRONOUSLY
button.click();
console.log("I: after click");
console.log("J: script end");
The Output 📜
A: script start
H: before click
B: click start
C: map: 1
C: map: 2
C: map: 3
D: class added
G: click end
I: after click
J: script end
E: Promise.then
F: setTimeout
Why This Happens (The Magic Mechanics) ⚙️
Look closely at I: after click. It appears after the entire click handler runs (logs B, C, D, and G).
Why? Because button.click() is a synchronous command.
The script runs H: before click.
It hits button.click() and immediately jumps into the addEventListener function.
It must finish that entire function (running through G: click end) before it can return to the main flow to print I: after click and J: script end.
The Rule: The train is moving. Passengers (code lines) exit strictly in boarding order.
Asynctasks (Promises likeEand Timeouts likeF) are thrown out of the window to catch a later train. 👋
🧪 Scenario 2: Immediate Magic (No Clicks)
console.log("A: script start");
const el = document.createElement("div");
// 🪄 Direct DOM mutation
el.classList.add("box");
console.log("B: class added");
// ⚠️ Forces Layout (Heavy synchronous calculation)
const height = el.getBoundingClientRect().height;
console.log("C: layout read", height);
Promise.resolve().then(() => {
console.log("D: Promise.then");
});
setTimeout(() => {
console.log("E: setTimeout");
}, 0);
console.log("F: script end");
The Output 📜
A: script start
B: class added
C: layout read 0
F: script end
and only then…
D: Promise.then
E: setTimeout
What Matters Here 🧠
-
classList.add()is synchronous. -
getBoundingClientRect()is synchronous (and expensive!). It forces the browser to calculate geometry right now. -
Promiseswait. Even if they feel “instant”, they are not allowed on the current train.
No async mechanism can intervene until the Call Stack is empty.
🚗 Where Render Fits In
Rendering (Layout, Paint, Composite) is not part of the Call Stack. Think of Render as the Flying Ford Anglia 🚙 trying to cross the train tracks.
- The Ford Anglia is hovering, ready to go.
- The engine is roaring (60fps needed!).
- The GPU is warmed up.
But: ⛔️ The Ford Anglia cannot cross while the Hogwarts Express 🚂 (Call Stack) is occupying the track.
It doesn’t matter how urgent the paint is. If your while loop is running, or your JSON is parsing, or your map() is iterating, the frame is dropped. The screen freezes. The illusion breaks.
🧑🎓 Who Actually Sits in the Call Stack?
When we say “Call Stack”, we don’t mean abstract code. We mean specific synchronous operations that hog the line:
✅ Array.map, filter, reduce
✅ for while loops
✅ classList.add() / remove()
✅ getBoundingClientRect() / offsetWidth
✅ Any function without await
All of them run to completion. All of them fully occupy the Main Thread. None of them let Render sneak in.
😈 Who Can Stop the Train?
The Call Stack cannot stop itself. Once the function starts, it runs until the closing brace.
Only external “Dark Arts” can stop it:
👑 The Browser (killing a frozen tab)
🧊 Debugger (breakpoints)
💥 Crash (Error)
This is not the Event Loop. This is just… The Line.
🧩 Part III Summary
- Synchronous execution always comes first.
- The Call Stack fully owns the Main Thread.
- Render cannot interrupt it.
- Promises and timers must wait for the stack to clear.
- The train moves until it has completed its route.
🔮 What’s Next?
We have the Line. Now we need the Loop.
In Part IV, we finally meet the messengers:
- 🦉 Microtasks
- 💌 Promises
Why does the Owl (Promise) arrive before the Render, but after the sync code? Stay tuned! ✨
💬 Let’s Discuss
Have you ever accidentally frozen your browser with a while(true) loop or heavy calculation? Drop your “horror stories” in the comments! 👇
Happy coding! 👩💻✨
Elina · WebMagic Mastering the Dark Arts of Web Development
