Event Loop
The Event Loop is the mechanism that lets JavaScript handle asynchronous operations even though JavaScript is single-threaded. The Event Loop continuously checks whether there are tasks to run and orchestrates their execution order.
Event Loop Components
┌──────────────────────────┐
│ Call Stack │ ← the function currently being executed
└──────────────────────────┘
↕
┌──────────────────────────┐
│ Web APIs / Node │ ← setTimeout, fetch, DOM events, etc.
└──────────────────────────┘
↓
┌──────────────────────────┐
│ Microtask Queue │ ← Promise.then, queueMicrotask, MutationObserver
│ (prioritized) │
└──────────────────────────┘
↓
┌──────────────────────────┐
│ Macrotask Queue │ ← setTimeout, setInterval, I/O, UI rendering
└──────────────────────────┘
The Call Stack
The Call Stack is the stack of functions currently being executed. JavaScript is single-threaded — only one function runs at a time.
function first() {
console.log('first');
}
function second() {
first();
console.log('second');
}
function third() {
second();
console.log('third');
}
third();
// Call Stack:
// 1. third() enters the stack
// 2. second() enters the stack (from third)
// 3. first() enters the stack (from second)
// 4. first() leaves the stack → prints "first"
// 5. second() continues → prints "second"
// 6. second() leaves the stack
// 7. third() continues → prints "third"
// 8. third() leaves the stack → stack is empty
// Output: "first" → "second" → "third"
The Microtask Queue
The microtask queue is processed EVERY TIME the call stack becomes empty, before any macrotask. This is what makes Promise.then always run faster than setTimeout.
console.log('1: Start');
setTimeout(() => {
console.log('2: setTimeout'); // macrotask
}, 0);
Promise.resolve().then(() => {
console.log('3: Promise'); // microtask
});
console.log('4: End');
// Output:
// 1: Start
// 4: End
// 3: Promise ← the microtask is processed first
// 2: setTimeout ← the macrotask is processed after the microtask completes
The Macrotask Queue
Macrotasks are processed after all microtasks are done. This includes setTimeout, setInterval, event handlers, and I/O callbacks.
console.log('Start');
setTimeout(() => {
console.log('setTimeout 1');
}, 0);
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
}).then(() => {
console.log('Promise 2');
});
queueMicrotask(() => {
console.log('Microtask 1');
});
console.log('End');
// Output:
// Start
// End
// Microtask 1 ← queueMicrotask = microtask
// Promise 1 ← Promise.then = microtask
// Promise 2 ← chained microtask
// setTimeout 1 ← macrotask
// setTimeout 2 ← macrotask
Code Explanation
Async/Await vs Promise Timing
async function example() {
console.log('A: Inside async function');
// Await pauses here; execution leaves the async function
// But it goes to a microtask, not a macrotask
await Promise.resolve();
console.log('B: After await');
}
example();
console.log('C: After calling example');
// Output:
// A: Inside async function
// C: After calling example
// B: After await ← await = microtask
// Difference from setTimeout
async function example2() {
console.log('1: Start');
setTimeout(() => {
console.log('2: setTimeout');
}, 0);
await Promise.resolve();
console.log('3: After await');
}
example2();
console.log('4: Outside');
// Output:
// 1: Start
// 4: Outside
// 3: After await ← await = microtask, faster than setTimeout
// 2: setTimeout ← macrotask
requestAnimationFrame
// requestAnimationFrame runs BEFORE the browser renders
// and after microtasks
// Ideal for animations and UI updates
console.log('Start');
requestAnimationFrame(() => {
console.log('requestAnimationFrame');
});
Promise.resolve().then(() => {
console.log('Promise');
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
// Output:
// Start
// Promise ← microtask (fastest)
// requestAnimationFrame ← before rendering
// setTimeout ← macrotask (slowest)
Real-World Examples
Understanding Execution Order for UI Updates
// COMMON MISTAKE: updating the UI before the DOM is ready
function loadDashboard() {
// This doesn't update the DOM immediately
fetch('/api/data')
.then(r => r.json())
.then(data => {
document.querySelector('#counter').textContent = data.count;
});
}
// CORRECT: make sure the DOM is ready first
document.addEventListener('DOMContentLoaded', function () {
loadDashboard();
});
Understanding When the UI Freezes
// FREEZES THE UI — all execution is synchronous
function processLargeData(data) {
console.log('Start process...');
// This loop BLOCKS the call stack until it's done
for (let i = 0; i < data.length; i++) {
// Heavy processing — the UI freezes during this
calculateStatistics(data[i]);
}
console.log('Processing complete');
}
// SOLUTION: break it up with setTimeout or chunking
function processDataChunk(data, chunkSize = 100) {
let index = 0;
function processChunk() {
const chunk = data.slice(index, index + chunkSize);
chunk.forEach(item => calculateStatistics(item));
index += chunkSize;
// Give the UI a chance to update
if (index < data.length) {
setTimeout(processChunk, 0); // yield to the event loop
} else {
console.log('All done');
updateUI();
}
}
processChunk();
}
// SOLUTION: use a Web Worker for heavy computation
// (see the Web Workers tutorial)
Unbounded Microtasks
// CAREFUL: unbounded microtasks can block macrotasks
function endlessMicrotask() {
Promise.resolve().then(() => {
endlessMicrotask(); // endless recursion
});
}
// The setTimeout will NEVER be called because the microtasks
// keep piling up
setTimeout(() => {
console.log('This will never appear');
}, 0);
endlessMicrotask();
Click Event + Promise Order
// When the user clicks a button
document.querySelector('#btn').addEventListener('click', async function () {
console.log('1: Button click');
// Await inside the event handler
const data = await fetch('/api/data').then(r => r.json());
console.log('2: Data direceive');
// Update the DOM
document.querySelector('#output').textContent = data.value;
console.log('3: DOM diupdate');
});
// Click the button → output:
// 1: Button click
// 2: Data direceive ← after the fetch resolves
// 3: DOM diupdated
// What happens:
// 1. Click → the event handler enters the call stack
// 2. fetch() starts, the handler leaves the call stack (waiting)
// 3. The fetch resolves → Promise.then (microtask) → the handler continues
// 4. "Data direceive" is printed
// 5. The DOM is updated, "DOM diupdate" is printed
Execution Order Summary
// 1. Synchronous code (Call Stack) — runs first
// 2. Microtask Queue — processed to completion once the call stack is empty
// - Promise.then/finally/catch
// - queueMicrotask()
// - MutationObserver
// 3. requestAnimationFrame — before rendering
// 4. Rendering — the browser repaints
// 5. Macrotask Queue — processed one at a time
// - setTimeout/setInterval
// - I/O callbacks
// - UI event handlers
Conclusion
The Event Loop is the heart of JavaScript. The Call Stack executes synchronous code, Web APIs handle async operations, and the Microtask Queue (Promises) is processed before the Macrotask Queue (setTimeout). Use await to yield control to the event loop, setTimeout(fn, 0) to split up heavy work, and avoid unbounded recursive microtasks that can block the UI.