HT
How Things Work

Event Loop & Call Stack

How JavaScript handles async operations with a single thread β€” task queue, microtask queue, and the render cycle.

How It Works

JavaScript is single-threaded, yet it handles thousands of concurrent operations. The secret is the Event Loop β€” a continuous cycle that coordinates the Call Stack, Microtask Queue, and Macrotask Queue to execute code without blocking.

1
Synchronous Execution

JavaScript is single-threaded. The engine executes code top-to-bottom, pushing function calls onto the Call Stack. Each function runs to completion before the next one starts.

2
Web APIs & Registration

When you call setTimeout, fetch, or addEventListener, the browser's Web APIs handle the async work outside the JS thread. When complete, the callback is placed into a queue.

3
Microtask Queue (High Priority)

Promises (.then, .catch, .finally), MutationObserver, and queueMicrotask() callbacks go into the Microtask Queue. This queue is drained COMPLETELY after every task β€” before the browser renders or picks up the next macrotask.

4
Macrotask Queue (Low Priority)

setTimeout, setInterval, I/O, and UI events go into the Macrotask Queue. The Event Loop picks ONE macrotask per cycle, executes it, then drains all microtasks before the next macrotask.

5
The Event Loop Cycle

1) Run all synchronous code β†’ 2) Drain the entire Microtask Queue β†’ 3) Browser may render β†’ 4) Pick ONE Macrotask β†’ 5) Repeat. This is why Promise callbacks always run before setTimeout(cb, 0).

Key Concepts

πŸ“šCall Stack

LIFO data structure tracking function execution. JavaScript can only do one thing at a time β€” the current stack frame.

⚑Microtask Queue

High-priority queue for Promise callbacks and queueMicrotask. Drained completely between every task.

⏰Macrotask Queue

Lower-priority queue for setTimeout, setInterval, I/O callbacks. One task processed per event loop tick.

πŸ”„Event Loop

The orchestrator that checks: is the stack empty? If yes, drain microtasks, then pick one macrotask. Repeat forever.

πŸƒRun-to-Completion

Each function on the call stack runs fully before yielding. You can't interrupt synchronous code mid-execution.

Event Loop Execution Order
tsx
1// Classic event loop puzzle:
2console.log("1: Script start");
3
4setTimeout(() => console.log("5: setTimeout"), 0);
5
6Promise.resolve()
7 .then(() => console.log("3: Promise 1"))
8 .then(() => console.log("4: Promise 2"));
9
10console.log("2: Script end");
11
12// Output order: 1, 2, 3, 4, 5
13// Why? Microtasks (Promises) run BEFORE macrotasks (setTimeout)
14// even though setTimeout(cb, 0) was registered first!
πŸ’‘
Why This Matters

Understanding the Event Loop is foundational to debugging async bugs, preventing UI freezes, and writing performant JavaScript. Every Promise, setTimeout, fetch, and event handler flows through this mechanism.

Common Pitfalls

⚠setTimeout(cb, 0) does NOT mean 'run immediately' β€” it means 'add to macrotask queue with minimum delay'. The actual delay depends on the queue.
⚠Microtasks can starve the render cycle. An infinite chain of Promise.then() calls will block painting because the microtask queue must be fully drained before rendering.
⚠async/await is syntactic sugar over Promises. The code after 'await' runs as a microtask, NOT synchronously.
⚠Node.js has a slightly different event loop with additional phases (timers, I/O callbacks, idle, poll, check, close). process.nextTick() runs even before Promise microtasks.
Real-World Use Cases

1UI Freezing from Long Synchronous Operations

Scenario

Your React app processes a 10,000-row CSV file using a synchronous loop. The UI completely freezes β€” buttons don't respond, animations stutter, and users think the app crashed.

Problem

The synchronous loop occupies the Call Stack for ~3 seconds. Since JavaScript is single-threaded, the Event Loop cannot process any UI events (clicks, scrolls) or render frames until the loop finishes. The browser literally cannot update the screen.

Solution

Break the work into chunks using requestIdleCallback or setTimeout(chunk, 0). Each chunk processes ~500 rows, then yields back to the Event Loop, allowing the browser to process pending UI events and render between chunks. Alternatively, move the processing to a Web Worker (separate thread).

πŸ’‘

Takeaway: Understanding the Event Loop reveals why long sync operations freeze the UI. The loop can't reach the 'render' or 'pick macrotask' phase if the call stack is never empty. Always yield for heavy computation.

2Race Condition Between setTimeout and Promises

Scenario

You register a setTimeout(cb, 0) to update state, then a Promise.then() to read that state. The Promise callback sees stale data because it runs BEFORE the timeout β€” even though setTimeout was called first.

Problem

Developers assume 'setTimeout(cb, 0)' means 'run immediately after current code'. But 0ms just means 'enqueue in the Macrotask Queue as soon as possible'. Promise callbacks go to the Microtask Queue, which has higher priority.

Solution

Never rely on execution order between different queue types. If order matters, chain operations using the same mechanism (all Promises, or all setTimeouts). For reading after a write, use await or .then() chaining to guarantee sequence.

πŸ’‘

Takeaway: The Microtask Queue is always drained before any Macrotask runs. This queue priority model explains most 'unexpected execution order' bugs in async JavaScript code.