Ask ten developers how the JavaScript event loop works, and you'll get eleven different answers. That's because the event loop is invisible. You can't console.log it. You can't set a breakpoint on it. You just have to know it's there, orchestrating everything.
This guide changes that. We'll walk through the event loop step by step, with code you can actually run and see in
The JavaScript Runtime: Four Moving Parts
JavaScript's runtime has four key components:
| Component | Role |
|---|---|
| Call Stack | Where functions execute. One at a time — JS is single-threaded. |
| Web APIs | Browser features (setTimeout, fetch, DOM events) running outside the main thread. |
| Task Queue | Callbacks from Web APIs waiting for the call stack to empty. |
| Microtask Queue | Promise callbacks. Always processed before the task queue. |
And then there's the event loop itself — the coordinator. Its job: when the call stack is empty, pick the next thing to run.
Step 1: The Call Stack — One Thing at a Time
JavaScript is single-threaded. It has one call stack and can do one thing at a time. When you call a function, it gets pushed onto the stack. When it returns, it gets popped off.
function greet(name) {
return 'Hello, ' + name;
}
function processUser(name) {
const message = greet(name);
console.log(message);
}
processUser('Supratik');
Here's what happens on the call stack:
processUser('Supratik')is pushed onto the stack- Inside it,
greet('Supratik')is pushed on top
greetreturns → popped off the stack
console.log(message)is pushed, executes, popped
processUserreturns → popped off. Stack is empty.
💡 The call stack is like a stack of plates. You can only add or remove from the top. If a function calls another function, the new one goes on top and must finish before we return to the one below it.
Step 2: Web APIs — Where Async Things Wait
When you call setTimeout, fetch, or add a DOM event listener, JavaScript doesn't handle the waiting itself. It hands the job off to Web APIs — features provided by the browser (or Node.js runtime).
console.log('Start');
setTimeout(() => {
console.log('Timer done');
}, 2000);
console.log('End');
Here's the flow:
console.log('Start')→ runs immediately on the call stack
setTimeout→ registers the callback with the Web API. The timer starts counting outside the call stack. setTimeout itself returns immediately.
console.log('End')→ runs immediately- After 2000ms, the Web API moves the callback to the task queue
- The event loop sees the call stack is empty → picks the callback → runs it
Output: Start → End → Timer done. The timer callback runs last, even though it was registered second.
👉
Step 5: The Event Loop Algorithm
Now we can state the event loop's actual algorithm:
- Run all synchronous code on the call stack until it's empty.
- Drain the entire microtask queue. If any microtask adds new microtasks, drain those too.
- Take one task from the task queue and push it onto the call stack.
- Go back to step 2. (Yes — microtasks are checked again after every single macrotask.)
That's it. The entire event loop is these four steps, on repeat, forever.
Solving the Original Puzzle
Now let's go back to our puzzle and trace through it:
console.log('A'); // 1. Synchronous → runs immediately
setTimeout(() =>
console.log('B'), 0); // 2. Registers with Web API → callback goes to TASK queue
Promise.resolve().then(()
=> console.log('C')); // 3. Already resolved → callback goes to MICROTASK queue
console.log('D'); // 4. Synchronous → runs immediately
Step by step:
console.log('A')→ prints A
setTimeout→ callback sent to Web API → moves to task queue
Promise.resolve().then()→ already resolved → callback goes to microtask queue
console.log('D')→ prints D
- Call stack is empty → drain microtask queue →
console.log('C')→ prints C
- Microtask queue empty → take from task queue →
console.log('B')→ prints B
Final output: A, D, C, B. Mystery solved.
Bonus: How async/await Fits In
async/await is just syntactic sugar over Promises. When you await something, everything after the await becomes a microtask.
async function demo() {
console.log('Before await');
await Promise.resolve();
console.log('After await'); // This is a microtask!
}
console.log('Start');
demo();
console.log('End');
Output:
Start
Before await
End
After await
"After await" runs as a microtask — same as if you'd written Promise.resolve().then(() => console.log('After await')).
👉 , paste any code snippet from this article, and watch every step unfold across the call stack, queues, and event loop — in real time. It's free.
SOCIAL SHARE CARD GENERATOR