The Complete JavaScript Engine: Call Stack, Event Loop, and the Two Queues
You open a webpage. You click a button. Something happens. But have you ever wondered — how does JavaScript actually decide what to do, and when to do it?
If you've ever seen code that seemed to run in a weird order, or heard terms like "event loop" or "async" thrown around and felt lost — this post is for you. We're going to build a complete mental map of how JavaScript thinks, from scratch, using nothing but a restaurant analogy and a few lines of code.
By the end of this post you'll understand exactly why JavaScript runs code in the order it does — and you'll never be surprised by it again.
📝 Note: This post was written with the assistance of AI. The content reflects my personal learning and understanding and should not be taken as professional advice. Please do your own research and due diligence before acting on anything written here.
🍽️ The Restaurant That Explains Everything
Imagine a restaurant with exactly one waiter.
This waiter is efficient, focused, and can only do one thing at a time. When you ask for water, he gets it immediately. But when you order pasta, he doesn't stand at the kitchen window staring until the food is ready. He writes the order down, hands it to the kitchen, and moves on — taking other orders, refilling drinks, bringing out dishes for other tables.
That waiter is JavaScript.
JavaScript can only do one thing at a time. It has a single thread of execution — one brain, one track, no multitasking. And just like the waiter, it has a clever system for handling things that take time, without freezing up and doing nothing while it waits.
That system has four main parts: the call stack, the event loop, the microtask queue, and the macrotask queue. Let's meet each one.
🥞 The Call Stack — JavaScript's Brain
The call stack is where JavaScript actually runs your code. Think of it like the waiter's current task — whatever is on top of the stack is what JavaScript is doing right now.
When your code runs, JavaScript reads it line by line, top to bottom, and pushes each task onto the stack. When a task finishes, it gets popped off, and the next one begins.
console.log("First"); // pushed onto stack, runs, popped off
console.log("Second"); // pushed onto stack, runs, popped off
console.log("Third"); // pushed onto stack, runs, popped off
Output:
First
Second
Third
Simple. Predictable. One thing at a time.
The call stack is synchronous — it runs code in the exact order it appears, with no skipping, no waiting, no interruptions.
But here's the problem. What if one of those lines takes a long time — like fetching data from a server, or reading a large file? If the call stack just sat there waiting, your entire webpage would freeze. Nothing would click. Nothing would scroll. Nothing would work.
That's the problem the rest of the system exists to solve.
🔄 The Event Loop — The Waiter's Instinct
The event loop is not a place where code runs. It's more like an instinct — a constant background check that JavaScript performs.
It asks one question, over and over:
"Is the call stack empty? Is there anything waiting to run?"
If the call stack is empty and something is waiting — the event loop picks it up and sends it in. If the call stack is busy — the event loop waits patiently.
Think of it like the waiter's instinct to check the kitchen window. He's not standing there staring — but every time he finishes a task, he glances over. Is there a dish ready? Should I bring something out?
The event loop is that glance. Constant, automatic, invisible.
console.log("Start"); // call stack: runs immediately
setTimeout(() => {
console.log("Later"); // this gets handed off, runs after 1 second
}, 1000);
console.log("End"); // call stack: runs immediately
Output:
Start
End
Later
"Start" and "End" run immediately. "Later" gets handed off and comes back after 1 second — delivered by the event loop once the call stack is free.
The event loop is the bridge between the call stack and everything that's waiting. It never stops checking.
📬 The Two Queues — Not All Waiting Is Equal
Here's where it gets interesting. When code gets handed off to run later, it doesn't just go into one big pile. It goes into one of two queues — and they have very different priorities.
Think of your email. You have two folders:
- A priority inbox — urgent messages you handle as soon as you put down your pen. You don't move on until this folder is completely empty.
- A regular inbox — important, but not urgent. You get to it after the priority inbox is cleared, one email at a time.
JavaScript works exactly the same way.
🔴 The Microtask Queue — Priority Inbox
The microtask queue holds async work that needs to run as soon as the current task is done — before anything else gets a chance.
| What | Example |
|---|---|
| Promise callbacks | .then(), .catch(), .finally() |
await resumptions | everything after an await keyword |
queueMicrotask() | manually queuing a microtask |
These were designed by JavaScript to have higher priority. The reason is reliability — Promises are often chained together, one step feeding into the next. For that chain to work correctly and predictably, each step needs to run immediately after the previous one, without anything else sneaking in between.
🟡 The Macrotask Queue — Regular Inbox
The macrotask queue holds async work that can wait its turn. These run one at a time, after the priority inbox is completely empty.
| What | Example |
|---|---|
| Timers | setTimeout(), setInterval() |
| UI events | clicks, keypresses, scroll |
setImmediate() | Node.js specific — covered in a later post |
Timers belong here because they already have a delay built in. UI events belong here because they're triggered by the outside world and can wait for current work to finish first.
By design, microtasks are JavaScript's way of saying: "This is a follow-up to something that just finished. Handle it before moving on to anything new."
⚖️ The Golden Rule
These four components always work together in the same order — and this order never breaks:
1. Run all synchronous code (call stack)
↓
2. Clear the ENTIRE microtask queue
↓
3. Run ONE macrotask
↓
4. Clear the ENTIRE microtask queue again
↓
5. Run the next macrotask
↓
6. Repeat forever
Notice step 4 — after every single macrotask, JavaScript checks the microtask queue again before running the next macrotask. Microtasks can never be skipped over. They always get priority.
Let's see all three layers together:
console.log("Start"); // synchronous
setTimeout(() => {
console.log("Timeout"); // macrotask — regular inbox
}, 0);
Promise.resolve().then(() => {
console.log("Promise"); // microtask — priority inbox
});
console.log("End"); // synchronous
Output:
Start
End
Promise
Timeout
Step by step:
"Start"— synchronous, runs immediatelysetTimeout— registered as a macrotask, goes to regular inboxPromise.resolve().then(...)— registered as a microtask, goes to priority inbox"End"— synchronous, runs immediately- Call stack is now empty — event loop checks priority inbox first
"Promise"— microtask runs- Priority inbox is empty — event loop picks up the macrotask
"Timeout"— macrotask runs
Notice something sneaky — setTimeout has a delay of 0 milliseconds. Zero! You'd think that means "run immediately." But it still prints last, because it's in the macrotask queue, and the Promise in the microtask queue always goes first.
setTimeout(..., 0)doesn't mean "run right now." It means "run as soon as possible — but only after everything more urgent is done." And Promises are always more urgent.
🔗 One More Thing — Chained Promises
Here's something subtle that trips a lot of people up. When you chain .then() calls together, they don't all queue up at once. Each one only enters the microtask queue after the previous one finishes.
console.log("Start"); // synchronous
Promise.resolve()
.then(() => {
console.log("First"); // microtask 1
})
.then(() => {
console.log("Second"); // microtask 2 — only queued after First runs
});
console.log("End"); // synchronous
Output:
Start
End
First
Second
"Second" doesn't jump the queue — it waits for "First" to complete, then enters the microtask queue as a brand new microtask.
Chained
.then()calls are sequential, not simultaneous. Each one is a new microtask that only gets created when the previous one resolves.
👀 See It With Your Own Eyes
Reading about the event loop is one thing. Watching it happen in real time is something else entirely.
JS Visualizer 9000 is a free tool that lets you paste any JavaScript code and watch the call stack, microtask queue, and macrotask queue animate step by step in real time.
Take the code snippet from above, paste it in, and hit play. Watch how each piece moves through the system. It's the single best way to turn this mental model into something you can actually see.
🧩 The Complete Picture
Here's how all four components fit together:
Your Code
↓
Call Stack ← runs synchronous code right now
↓
Event Loop ← checks constantly: is stack empty?
↓
Microtask Queue 🔴 ← priority inbox, cleared completely first
↓
Macrotask Queue 🟡 ← regular inbox, one at a time
↓
[check microtasks again after every macrotask]
↓
[repeat forever]
Every line of JavaScript you ever write flows through this system. Once you see it, you start to understand why code sometimes runs in an order that surprised you — and you'll never be confused by it again.
✅ Key Takeaways
- JavaScript has one brain — the call stack — and can only do one thing at a time
- The event loop constantly checks if the call stack is empty and delivers waiting tasks
- The microtask queue holds high priority async work — Promises and await
- The macrotask queue holds regular async work — setTimeout, UI events
- Microtasks always drain completely before any macrotask runs — no exceptions, ever
- After every macrotask, JavaScript checks the microtask queue again before moving on
setTimeout(..., 0)still runs last — zero delay does not mean run immediately- Chained
.then()calls queue one at a time — each waits for the previous to finish
You now see JavaScript not as a language that just "runs code" — but as a carefully orchestrated system with a waiter, a priority inbox, and a very strict sense of order.
🔮 What's Next
In Post 2, we go deeper into Promises and async/await — what they actually are, how await pauses a function without freezing the whole program, and why they were invented in the first place.
This is Part 1 of a 5-part series on the JavaScript Event Loop.

