Ever wondered why console.log() inside a setTimeout(..., 0) runs after everything else?
Or why your Promise logs show up before your timer, even if both seem async?
Welcome to the world of the Event Loop — one of JavaScript’s most powerful (and misunderstood) features.
In this post, I’ll walk you through exactly how the Event Loop works using:
- Minimal theory
- Real-world examples
- Simple visual diagrams
Let’s dig in.
What’s Inside the JavaScript Runtime?
Before we talk about the Event Loop, here’s a quick overview of the runtime environment.
+--------------------+
| Call Stack | ← Executes your functions
+--------------------+
↓
+--------------------+
| Web APIs | ← Timers, fetch, DOM
+--------------------+
↓
+------------------------+
| Macrotask Queue | ← setTimeout, I/O
+------------------------+
↓
+------------------------+
| Microtask Queue | ← Promises, async/await
+------------------------+
↓
+--------------------+
| Event Loop | ← Coordinates everything
+--------------------+
Synchronous Example
function sayHi() {
console.log("Hi");
}
sayHi();
This runs top to bottom inside the call stack.
Asynchronous Example with setTimeout
console.log("Start");
setTimeout(() => {
console.log("Timer");
}, 0);
console.log("End");
Output:
Start
End
Timer
Why?
Because setTimeout doesn’t go directly on the call stack. It goes to Web APIs, waits for the timer, and then gets added to the macrotask queue, which only runs when the call stack is empty.
Microtasks vs Macrotasks
Try this code:
console.log("Start");
Promise.resolve().then(() => {
console.log("Microtask");
});
setTimeout(() => {
console.log("Macrotask");
}, 0);
console.log("End");
Output:
Start
End
Microtask
Macrotask
- Promises (microtasks) always run before setTimeouts (macrotasks), even if both are async.
- That’s how the Event Loop prioritizes execution.
Visual: The Event Loop Flow
[ Is Call Stack Empty? ]
↓
Run All Microtasks
↓
Run One Macrotask
↓
Repeat
This is what makes JavaScript non-blocking — and yet single-threaded.
async/await in Action
async function demo() {
console.log("1");
await Promise.resolve();
console.log("2");
}
demo();
console.log("3");
Output:
1
3
2
await splits the function. The part after await is queued as a microtask, and it runs after the current call stack completes.
Takeaways
- JavaScript is single-threaded, but async thanks to the Event Loop.
- Microtasks (Promises) run before Macrotasks (setTimeout, fetch, etc.).
- Use this mental model to debug better and write cleaner async code.
Want to Visualize It?
I've created this Event Loop diagram to help you internalize the flow:
Wrapping Up
The Event Loop isn’t just theory — it directly affects how your code runs. Whether you’re dealing with React effects, API calls, or DOM events, mastering this concept can help you avoid weird bugs and timing issues.
Let me know what confused you the most about the Event Loop when you first learned it.
And if you found this helpful, consider giving it a ❤️ or sharing it with a fellow developer.