Introduction
1 . The JavaScript Runtime Environment
2 . Single-Threaded Execution
3 . The Call Stack
4 . Web APIs (in Browsers)
5 . The Event Loop’s Core Job
6 . Macrotasks vs Microtasks
7 . Microtask Queue Priority
8 . setTimeout Gotchas
9 . Promises and the Event Loop
10 . Async/Await Behind the Scenes
11 . Long Tasks Blocking the Event Loop
12 . setInterval and the Event Loop
13 . requestAnimationFrame Timing
14 . Node.js Event Loop Phases
15 . Practical Debugging of Event Loop Issues
16 . Combining Microtasks & Macrotasks for Control
17 . Visualizing the Event Loop
Conclusion
JavaScript is often called “single-threaded” and “non-blocking” in the same breath. Wait… how can it be both? How can a single-threaded language handle async tasks like network calls, timers, and UI rendering without freezing?
The answer lies in the Event Loop, a deceptively simple mechanism that gives JavaScript its asynchronous magic.
If you truly understand the event loop, you’ll unlock:
In this guide, we’ll break it down step-by-step.
Definition: The runtime is the environment where JavaScript executes, typically a browser or Node.js.
Description: It consists of:
Code Example:
console.log("Hello from the runtime!");
This runs in the call stack immediately.
Definition: JavaScript executes code on one main thread, one line at a time.
Description: Only one piece of code runs at any given moment. There’s no parallel execution (except in Web Workers).
Code Example:
console.log("Step 1");
console.log("Step 2");
console.log("Step 3");
Always prints in order.
Definition: A stack data structure that tracks which functions are currently running.
Description: Functions are pushed when called, and popped when done.
Code Example:
function a() { b(); }
function b() { c(); }
function c() { console.log("Inside C"); }
a();
Execution order: a → b → c.
Definition: Browser-provided features for async operations: setTimeout, DOM events, fetch, etc.
Description: These run outside the call stack and hand results back later.
Code Example:
console.log("Start");
setTimeout(() => console.log("Timer done"), 1000);
console.log("End");
Output:
Start
End
Timer done
Definition: The Event Loop checks if the call stack is empty, then pushes queued callbacks onto it.
Description: It’s like a traffic cop, making sure tasks don’t block each other.
Code Example:
console.log("Script start");
setTimeout(() => {
console.log("Timeout callback");
}, 0);
console.log("Script end");
Output order proves async nature.
Definition: Macrotasks: setTimeout, setInterval, setImmediate
Microtasks: Promise callbacks, queueMicrotask, MutationObserver
Description: Microtasks run before the next rendering or macrotask.
Code Example:
setTimeout(() => console.log("Macrotask"), 0);
Promise.resolve().then(() => console.log("Microtask"));
console.log("Script");
Output:
Script
Microtask
Macrotask
Definition: Microtasks are processed immediately after the current task, before rendering.
Description: Promises often run before timers.
Code Example:
setTimeout(() => console.log("Timer"), 0);
queueMicrotask(() => console.log("Queue Microtask"));
console.log("Main");
Output:
Main
Queue Microtask
Timer
Definition: setTimeout(fn, 0) means minimum delay, not “instant”.
Description: It’s scheduled after current call stack and microtasks finish.
Code Example:
setTimeout(() => console.log("Timeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
console.log("Sync");
Output:
Sync
Promise
Timeout
Definition: Promises resolve into microtasks.
Description: They run faster than timers but still after synchronous code.
Code Example:
Promise.resolve().then(() => console.log("Promise 1"));
console.log("Sync");
Promise.resolve().then(() => console.log("Promise 2"));
Output:
Sync
Promise 1
Promise 2
Definition: async/await is syntactic sugar for Promises.
Description: await pauses the function until the Promise resolves, resuming as a microtask.
Code Example:
async function run() {
console.log("Before await");
await null;
console.log("After await");
}
run();
console.log("Outside");
Output:
Before await
Outside
After await
Definition: A long-running function can freeze the UI because the event loop can’t process other tasks.
Code Example:
console.log("Start");
for (let i = 0; i < 1e9; i++) {} // Block
console.log("End");
No other code runs until this finishes.
Definition: setInterval schedules repeated tasks but can be delayed if the loop is busy.
Code Example:
setInterval(() => console.log("Tick"), 1000);
If you block the loop, ticks are delayed.
Definition: Browser API for smooth animations, synced with refresh rate.
Description: Runs before the next repaint.
Code Example:
function draw() {
console.log("Drawing");
requestAnimationFrame(draw);
}
draw();
Definition: Node’s loop has phases: timers, I/O callbacks, idle, poll, check, close callbacks.
Description: setImmediate vs setTimeout behave differently.
Code Example (Node.js only):
setImmediate(() => console.log("Immediate"));
setTimeout(() => console.log("Timeout"), 0);
console.log("Main");
Definition: Understanding the loop helps fix async race conditions.
Code Example:
let data;
fetch("/api").then(res => res.json()).then(json => {
data = json;
});
console.log(data); // undefined (fetch not done yet)
Solution: Work inside the async callback or use await.
Description: Sometimes you need to control execution order.
Code Example:
Promise.resolve().then(() => console.log("Microtask 1"));
setTimeout(() => console.log("Macrotask 1"), 0);
Promise.resolve().then(() => console.log("Microtask 2"));
Description: It’s helpful to think:
The Event Loop isn’t just a quirky detail, it’s the heartbeat of JavaScript. Understanding it means you can:
From call stack to microtasks and macrotasks, mastering the event loop gives you true control over how and when your code runs.
Software Engineer
Senior Software Engineer