JavaScript Event Loop - Complete Guide
Table of Contentsโ
- What is the Event Loop?
- Why the Event Loop Exists
- Core Components
- Execution Order
- Simple Examples
- Microtask Queue Behavior
- Rendering and Event Loop
- setTimeout(fn, 0) - Common Misconception
- Event Loop in Node.js
- Starvation Problem
- Event Loop vs Execution Context
- Real-World React Relevance
- Common Interview Questions
- Mental Model
- Tricky Interview Snippets
What is the Event Loop?โ
The Event Loop is the mechanism that allows JavaScript (which is single-threaded) to perform non-blocking asynchronous operations by coordinating the Call Stack, task queues, and host APIs (like browser APIs or Node.js APIs).
Key Point:โ
JavaScript can only execute one thing at a time, but the Event Loop enables concurrent behavior through asynchronous callbacks.
Why the Event Loop Existsโ
JavaScript has fundamental constraints:
- One Call Stack - can only execute one function at a time
- Single-threaded - no parallel execution of JavaScript code
However, browsers and Node.js must handle:
- โฑ๏ธ Timers (
setTimeout,setInterval) - ๐ Network calls (
fetch, AJAX) - ๐ฑ๏ธ DOM events (clicks, scrolls)
- โจ Promises
- ๐ก I/O operations
Solution: The Event Loop makes asynchronous operations possible without blocking the main thread.
Core Componentsโ
Understanding these components is essential for mastering the Event Loop:
1. Call Stackโ
The Call Stack executes JavaScript execution contexts using a LIFO (Last In First Out) structure.
function a() { b(); }
function b() { console.log('b'); }
a();
Call Stack Visualization:
| b() | โ Top (executes first)
| a() |
| GEC | โ Bottom (Global Execution Context)
2. Web APIs (Browser Environment)โ
These APIs are not part of the JavaScript engine but are provided by the host environment:
setTimeout/setIntervalfetch/XMLHttpRequest- DOM events (
addEventListener) requestAnimationFrameMutationObserver
In Node.js: File system, network operations, timers, etc.
3. Task Queuesโ
JavaScript has two types of task queues with different priorities:
(A) Macrotask Queue (Lower Priority)โ
Also called the Task Queue or Callback Queue:
setTimeoutsetIntervalsetImmediate(Node.js only)- I/O callbacks
- UI rendering tasks
- User interaction events
(B) Microtask Queue (Higher Priority) ๐ฅโ
Also called the Job Queue:
Promise.then/catch/finallyqueueMicrotask()MutationObserverprocess.nextTick()(Node.js - highest priority)
4. Event Loopโ
The Event Loop continuously monitors and coordinates:
- Is the Call Stack empty?
- Run ALL microtasks (drain the microtask queue)
- Take ONE macrotask from the macrotask queue
- Render (if needed in browser)
- Repeat
Execution Orderโ
This is the MOST IMPORTANT concept to understand:
1. Synchronous Code (Call Stack)
โ
2. Microtask Queue (drain completely)
โ
3. Macrotask Queue (execute ONE task)
โ
4. Render (if needed)
โ
5. Repeat from step 2
Priority Hierarchy:โ
Highest Priority: Call Stack (synchronous code)
โ
High Priority: Microtasks (Promises)
โ
Low Priority: Macrotasks (setTimeout)
โ
Lowest: Rendering
Simple Examplesโ
Example 1: Basic Event Loopโ
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');
Execution Steps:โ
'A'โ logged immediately (synchronous)setTimeoutโ callback sent to Macrotask QueuePromise.thenโ callback sent to Microtask Queue'D'โ logged immediately (synchronous)- Call Stack empty โ drain Microtask Queue โ
'C'logged - Take one Macrotask โ
'B'logged
Output:โ
A
D
C
B
Example 2: Multiple Promisesโ
console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve()
.then(() => console.log('Promise 1'))
.then(() => console.log('Promise 2'));
console.log('End');
Output:โ
Start
End
Promise 1
Promise 2
Timeout
Why? All microtasks run before any macrotask.
Microtask Queue Behaviorโ
Critical Rule: Microtasks Drain Completelyโ
The microtask queue must be completely empty before moving to macrotasks. This can lead to interesting behavior:
setTimeout(() => console.log('timeout'));
Promise.resolve().then(() => {
console.log('promise');
Promise.resolve().then(() => console.log('inner promise'));
});
Output:โ
promise
inner promise
timeout
Why?โ
- First
Promise.thenruns (microtask) - It logs
'promise' - It creates another microtask (
'inner promise') - That new microtask runs immediately (queue must drain)
- Only then does
setTimeoutrun (macrotask)
Trap Question:โ
setTimeout(() => console.log('1'));
Promise.resolve().then(() => {
console.log('2');
setTimeout(() => console.log('3'));
});
Promise.resolve().then(() => console.log('4'));
Output:โ
2
4
1
3
Explanation:
- All promises resolve first (microtasks)
- Then first
setTimeoutruns (macrotask from beginning) - Then second
setTimeoutruns (macrotask added during microtask)
Rendering and Event Loopโ
In browsers, rendering happens:
- After a macrotask completes
- After all microtasks are drained
- Before the next macrotask
Example:โ
const element = document.getElementById('box');
setTimeout(() => {
element.style.display = 'block';
// Rendering happens AFTER this callback finishes
}, 0);
Performance Implication:โ
// BAD - Blocks rendering
for (let i = 0; i < 1000; i++) {
Promise.resolve().then(() => heavyCalculation());
}
// BETTER - Allows rendering between tasks
for (let i = 0; i < 1000; i++) {
setTimeout(() => heavyCalculation(), 0);
}
setTimeout(fn, 0) - Common Misconceptionโ
Misconception: It runs immediatelyโ
โ FALSE
Reality: It's still asynchronous and has delaysโ
console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
console.log('End');
Output:
Start
End
Timeout
Why setTimeout(fn, 0) is NOT immediate:โ
- Minimum delay - Browsers enforce a 4ms minimum delay (after 5 nested calls)
- Waits for Call Stack - Must wait for synchronous code to finish
- Waits for Microtasks - All promises must resolve first
- Queue position - Must wait for other macrotasks ahead of it
Event Loop in Node.jsโ
Node.js has a more complex event loop with multiple phases:
Node.js Event Loop Phases:โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโ>โ timers โ (setTimeout, setInterval)
โ โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
โ โ pending callbacks โ (I/O callbacks)
โ โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
โ โ idle, prepare โ (internal use)
โ โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
โ โ poll โ (retrieve new I/O events)
โ โโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโ
โ โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
โ โ check โ (setImmediate)
โ โโโโโโโโโโโโโโโฌโโโโโ โโโโโโโโโ
โ โโโโโโโโโโโโโโโดโโโโโโโโโโโโโโ
โโโโ close callbacks โ (socket.on('close'))
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Node.js Priority Order:โ
Highest: process.nextTick()
โ
High: Promise (microtasks)
โ
Medium: setTimeout / setInterval
โ
Low: setImmediate
Node.js Example:โ
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
Output:
nextTick
Promise
setTimeout
setImmediate
Starvation Problemโ
What is Starvation?โ
When microtasks keep adding more microtasks, preventing macrotasks from ever running.
Example:โ
function recurse() {
Promise.resolve().then(recurse);
}
recurse();
// UI completely freezes! โ
Why This is Bad:โ
- Microtask queue never empties
- Macrotasks never run
- Browser cannot render
- UI becomes completely unresponsive
Solution:โ
function recurse() {
setTimeout(recurse, 0); // Use macrotask instead
}
recurse();
// UI can still render between tasks โ
Event Loop vs Execution Contextโ
These are different but related concepts:
| Concept | Role | Storage | Lifetime |
|---|---|---|---|
| Execution Context | Runs code | Call Stack | Temporary (popped after execution) |
| Call Stack | Stores execution contexts | Memory | Cleared when empty |
| Event Loop | Schedules tasks | Coordinates queues | Runs continuously |
Relationship:โ
Event Loop โ Pushes callbacks to Call Stack
Call Stack โ Executes code in Execution Contexts
Execution Context โ Runs JavaScript code
Real-World React Relevanceโ
Understanding the Event Loop is crucial for React performance:
1. State Updates are Batchedโ
function handleClick() {
setState1(value1); // Batched
setState2(value2); // Batched
setState3(value3); // Batched
// Single re-render after all updates
}
2. useEffect Runs in Microtasksโ
useEffect(() => {
// This runs as a microtask after render
console.log('Effect runs');
}, []);
3. Long Microtasks Delay Paintโ
// BAD - Blocks rendering
Promise.resolve().then(() => {
for (let i = 0; i < 1000000; i++) {
// Heavy computation
}
});
4. Heavy Synchronous Work Blocks Renderingโ
// BAD - Freezes UI
function handleClick() {
for (let i = 0; i < 1000000; i++) {
// Blocks the main thread
}
}
// BETTER - Break into chunks
function handleClick() {
let i = 0;
function chunk() {
for (let j = 0; j < 1000 && i < 1000000; j++, i++) {
// Process small chunk
}
if (i < 1000000) {
setTimeout(chunk, 0); // Allow rendering between chunks
}
}
chunk();
}
Common Interview Questionsโ
Q: Are Promises asynchronous?โ
โ No - The Promise constructor executes synchronously
โ
Yes - Their .then(), .catch(), .finally() callbacks are asynchronous (microtasks)
console.log('1');
new Promise((resolve) => {
console.log('2'); // Synchronous!
resolve();
}).then(() => console.log('3')); // Asynchronous (microtask)
console.log('4');
// Output: 1, 2, 4, 3
Q: Why does Promise run before setTimeout?โ
Answer: Microtasks (Promises) have higher priority than macrotasks (setTimeout). The microtask queue must be completely drained before any macrotask runs.
Q: Can the Event Loop run when the Call Stack is not empty?โ
โ No - The Event Loop only moves tasks from queues to the Call Stack when the Call Stack is completely empty.
Q: What happens if microtasks keep creating new microtasks?โ
Answer: The microtask queue will never empty, causing starvation - macrotasks never run, rendering is blocked, and the UI freezes.
Q: Does the browser render between microtasks?โ
โ No - Rendering only happens after all microtasks complete.
Mental Modelโ
The Golden Rule:โ
1. Run all synchronous code (Call Stack)
โ
2. Empty the Call Stack
โ
3. Drain ALL microtasks (Promises)
โ
4. Run ONE macrotask (setTimeout)
โ
5. Render (if in browser)
โ
6. Repeat from step 3
Visual Mental Model:โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Call Stack (JS Code) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Execution Context (EC) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ (when empty)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Microtask Queue (Priority) โ
โ โข Promise.then โ
โ โข queueMicrotask โ
โ โข MutationObserver โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ (drain completely)
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Macrotask Queue (Regular) โ
โ โข setTimeout โ
โ โข setInterval โ
โ โข I/O โ
โโโโโโโโโโโโโโโโฌโโโโโ โโโโโโโโโโโโโโโโโโโ
โ (take ONE)
โ
[Render if needed]
โ
โ
[Repeat โป]
Tricky Interview Snippetsโ
Snippet 1:โ
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => {
console.log('3');
setTimeout(() => console.log('4'), 0);
});
Promise.resolve().then(() => console.log('5'));
console.log('6');
Click for output and explanation
Output:
1
6
3
5
2
4
Explanation:
1- synchronous6- synchronous3- first microtask5- second microtask2- first macrotask (from beginning)4- second macrotask (added during microtask)
Snippet 2:โ
setTimeout(() => console.log('A'), 0);
Promise.resolve()
.then(() => console.log('B'))
.then(() => console.log('C'));
setTimeout(() => console.log('D'), 0);
Promise.resolve().then(() => console.log('E'));
console.log('F');
Click for output and explanation
Output:
F
B
E
C
A
D
Explanation:
F- synchronousB- first microtask (first promise chain)E- second microtask (second promise)C- third microtask (continuation of first chain)A- first macrotaskD- second macrotask
Snippet 3 (Advanced):โ
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise in Timeout 1'));
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
setTimeout(() => console.log('Timeout in Promise 1'), 0);
})
.then(() => console.log('Promise 2'));
setTimeout(() => console.log('Timeout 2'), 0);
console.log('End');
Click for output and explanation
Output:
Start
End
Promise 1
Promise 2
Timeout 1
Promise in Timeout 1
Timeout 2
Timeout in Promise 1
Explanation:
Start,End- synchronousPromise 1,Promise 2- drain microtask queueTimeout 1- first macrotaskPromise in Timeout 1- microtask from macrotask (runs before next macrotask)Timeout 2- second macrotask (from beginning)Timeout in Promise 1- third macrotask (added during first promise)
Killer Interview One-Linerโ
"The event loop coordinates the call stack and task queues to enable non-blocking async execution, always prioritizing microtasks over macrotasks, ensuring the call stack is empty before processing any queued tasks."
Key Takeawaysโ
- โ JavaScript is single-threaded but achieves concurrency through the Event Loop
- โ Microtasks (Promises) always run before macrotasks (setTimeout)
- โ The Call Stack must be empty before the Event Loop processes queues
- โ Microtasks drain completely before taking one macrotask
- โ Rendering happens after macrotasks and microtasks
- โ Infinite microtasks cause UI starvation
- โ
setTimeout(fn, 0)is not immediate - it's still asynchronous - โ Understanding the Event Loop is crucial for React performance and debugging async issues
Further Learningโ
To deepen your understanding, explore:
- Event Loop + Browser Rendering Pipeline - how painting and compositing work
- Request Animation Frame - optimal timing for animations
- Web Workers - true parallelism in JavaScript
- Async/Await internals - how they use the Event Loop
- Performance monitoring - using Chrome DevTools Performance tab