The Event Loop
How JavaScript handles async. Single-threaded but never blocked.
Single Thread
JavaScript runs one thing at a time — always.
// JavaScript has ONE thread of execution.
// It can only do one thing at a time.
console.log("First"); // runs
console.log("Second"); // waits for First to finish
console.log("Third"); // waits for Second to finish
// So how does async work?
// The EVENT LOOP coordinates between:
// 1. The Call Stack (where code runs)
// 2. Web APIs (where async operations wait)
// 3. Task Queues (where callbacks line up)
// JS itself is single-threaded,
// but the BROWSER is not — it has many threads
// for timers, network, DOM, etc.The actors:
Call Stack — where functions execute (one at a time)
Web APIs — browser handles timers, fetch, DOM events
Task Queue — callbacks waiting for the stack to empty
Event Loop — moves tasks from queue → stack when empty
Call Stack
The stack tracks what's currently executing.
function multiply(a, b) { return a * b; }
function square(n) { return multiply(n, n); }
function printSquare(n) {
const result = square(n);
console.log(result);
}
printSquare(4);
// Call Stack progression:
// 1. [printSquare]
// 2. [printSquare, square]
// 3. [printSquare, square, multiply]
// 4. [printSquare, square] ← multiply returns
// 5. [printSquare] ← square returns
// 6. [printSquare, console.log]
// 7. [printSquare] ← console.log returns
// 8. [] ← printSquare returnsFunctions push onto the stack when called, pop off when they return. Only the top frame is executing.
1 / 2
The Loop
The event loop's job: move queued tasks to the stack.
// The Event Loop algorithm (simplified):
//
// while (true) {
// 1. Run all synchronous code until the stack is empty
// 2. Run ALL microtasks (promises, queueMicrotask)
// 3. Render update (if needed — ~60fps = every 16ms)
// 4. Pick ONE macrotask from the queue (setTimeout, etc.)
// 5. Go back to step 1
// }
// Visual:
//
// ┌─────────────────────────────┐
// │ Call Stack │ ← code runs here
// └─────────────┬───────────────┘
// │ empty?
// ┌─────────────▼───────────────┐
// │ Microtask Queue │ ← drain ALL
// └─────────────┬───────────────┘
// │ empty?
// ┌─────────────▼───────────────┐
// │ Render (if needed) │
// └─────────────┬───────────────┘
// │
// ┌─────────────▼───────────────┐
// │ Macrotask Queue │ ← pick ONE
// └─────────────┬───────────────┘
// │
// loop back ↑Task Queue
Where async callbacks wait their turn.
console.log("1: start");
setTimeout(() => {
console.log("2: timeout"); // → macrotask queue
}, 0);
console.log("3: end");
// Output: "1: start", "3: end", "2: timeout"
// Why? Even with 0ms delay:
// 1. console.log("1: start") → runs immediately (stack)
// 2. setTimeout callback → goes to Web API, then queue
// 3. console.log("3: end") → runs immediately (stack)
// 4. Stack is now empty → event loop picks up timeout callback
// 5. console.log("2: timeout") → finally runs
// The key insight:
// setTimeout(fn, 0) doesn't mean "run immediately"
// It means "run as soon as the stack is empty"
// (minimum delay is actually ~4ms in browsers)// Multiple tasks queue in order:
setTimeout(() => console.log("A"), 0);
setTimeout(() => console.log("B"), 0);
setTimeout(() => console.log("C"), 0);
// Output: A, B, C (FIFO — first in, first out)
// Longer delays queue later:
setTimeout(() => console.log("slow"), 100);
setTimeout(() => console.log("fast"), 10);
// Output: fast, slow (10ms fires before 100ms)
// But remember: times are MINIMUM delays
// If the stack is busy, the callback waits longer:
setTimeout(() => console.log("delayed"), 10);
// If synchronous code takes 500ms,
// this callback runs after ~500ms, not 10msExecution Order
Predict the order of async operations.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1, 4, 3, 2
//
// Why this order:
// "1" — synchronous, runs immediately
// setTimeout → callback goes to MACROTASK queue
// Promise.then → callback goes to MICROTASK queue
// "4" — synchronous, runs immediately
// Stack empty → drain microtasks first → "3"
// Then pick one macrotask → "2"
// Rule: Microtasks ALWAYS run before macrotasks
// (when both are waiting)// More complex example:
console.log("A");
setTimeout(() => {
console.log("B");
Promise.resolve().then(() => console.log("C"));
}, 0);
Promise.resolve().then(() => {
console.log("D");
setTimeout(() => console.log("E"), 0);
});
console.log("F");
// Output: A, F, D, B, C, E
// A — sync
// F — sync
// D — microtask (runs before any macrotask)
// B — first macrotask (setTimeout from top)
// C — microtask created inside B's macrotask
// E — second macrotask (setTimeout from D's microtask)FAQ
Common questions about the event loop.