JavaScript Event Loop
debt(d7/e7/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The detection_hints list chrome-devtools and lighthouse as tools — these can surface jank or long tasks in profiling/performance panels, but only after the fact at runtime. There is no static analysis that reliably catches event-loop-blocking patterns before deployment; the symptom (UI freeze, delayed callbacks) only manifests under real load or careful profiling.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix says to move heavy computation to a Web Worker or chunk it with requestIdleCallback. Introducing Web Workers requires a new threading boundary, message-passing architecture, and rethinking data flow — this is not a single-file change. Long blocking synchronous operations may be spread across many call sites, making remediation a cross-cutting concern rather than a localised patch.
Closest to 'persistent productivity tax' (d5, scored b5). applies_to is 'web' context only, limiting reach somewhat compared to universal concerns. However, the event loop is a fundamental JavaScript execution model — every async feature (Promises, setTimeout, fetch, UI events) is shaped by it. Teams must continuously reason about microtask vs macrotask ordering, blocking vs non-blocking code, and main-thread budget, imposing an ongoing productivity tax across many work streams.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The misconception field explicitly states that setTimeout(fn, 0) is widely believed to execute 'immediately after the current line' but actually runs after the call stack clears AND all pending microtasks (Promises) resolve. This ordering (microtasks before macrotasks) contradicts how developers reason about 'zero delay' and is a well-documented but frequently missed gotcha that causes subtle ordering bugs.
Also Known As
TL;DR
Explanation
JavaScript runs on a single thread. The event loop coordinates: the Call Stack (synchronous execution), the Microtask Queue (Promises, queueMicrotask — drained completely after every task), and the Macrotask Queue (setTimeout, setInterval, I/O callbacks — one per loop iteration). When the stack is empty, microtasks run first (all of them), then one macrotask, then microtasks again, and so on. Understanding this explains why Promise.resolve().then() fires before setTimeout(fn, 0), and why a long synchronous task blocks the UI — it starves the event loop. In PHP terms this maps conceptually to Swoole's coroutine scheduler or ReactPHP's event loop, though PHP isolates workers per request by default.
Diagram
flowchart TD
CALL[Call Stack] -->|empty?| EL{Event Loop}
EL -->|check first| MICRO[Microtask Queue<br/>Promise.then / await]
MICRO -->|drain completely| CALL
EL -->|then check| MACRO[Macrotask Queue<br/>setTimeout / setInterval / I/O]
MACRO -->|one at a time| CALL
WEB[Web APIs<br/>fetch, timers, DOM] -->|callback ready| MACRO
INFO[Microtasks always drain<br/>before next macrotask]
style MICRO fill:#238636,color:#fff
style MACRO fill:#1f6feb,color:#fff
style CALL fill:#6e40c9,color:#fff
Common Misconception
Why It Matters
Common Mistakes
- Long synchronous operations that block the event loop — the UI freezes and no events are processed.
- Not understanding that Promise callbacks (microtasks) run before setTimeout callbacks (macrotasks).
- Assuming setTimeout(fn, 0) runs immediately — it runs after all microtasks and the current synchronous code.
- Heavy computation in the main thread instead of a Web Worker.
Code Examples
// Blocking the event loop — UI freezes:
function processLargeArray(arr) {
for (let i = 0; i < arr.length; i++) {
heavyComputation(arr[i]); // Blocks for 5s — page unresponsive
}
}
// Non-blocking — yield to event loop:
async function processChunked(arr) {
for (let i = 0; i < arr.length; i += 1000) {
processChunk(arr.slice(i, i + 1000));
await new Promise(r => setTimeout(r, 0)); // Yield
}
}
// Microtasks (Promises) always run before the next macrotask (setTimeout)
console.log('1 — sync');
setTimeout(() => console.log('4 — macrotask'), 0);
Promise.resolve().then(() => console.log('2 — microtask'));
console.log('3 — sync');
// Output: 1, 3, 2, 4
// Blocking the event loop — don't do this:
function blockFor(ms) {
const end = Date.now() + ms;
while (Date.now() < end) {} // busy wait — UI freezes
}
// Non-blocking alternative:
await new Promise(resolve => setTimeout(resolve, ms));
// Break up long CPU work:
async function processLargeArray(items) {
for (let i = 0; i < items.length; i++) {
process(items[i]);
if (i % 1000 === 0) await scheduler.yield(); // breathe
}
}