JavaScript Event Loop
Understand how the JavaScript event loop works under the hood and how to write non-blocking code.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
The JavaScript event loop is the heart of asynchronous programming in browsers and Node.js. It orchestrates the execution of code, collects and processes events, and executes queued sub-tasks. Understanding how the call stack, task queue, and microtask queue interact is essential for writing performant, non-blocking applications.
When to Use
Use this resource when:
- Debugging mysterious asynchronous bugs or race conditions
- Optimizing UI responsiveness in frontend applications
- Choosing between setTimeout, Promise, and queueMicrotask
- Understanding why code order does not always match execution order
Solution
Visualizing the Event Loop
console.log('1. Script start');
setTimeout(() => {
console.log('2. setTimeout (macrotask)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise (microtask)');
});
queueMicrotask(() => {
console.log('4. queueMicrotask');
});
console.log('5. Script end');
// Output order:
// 1. Script start
// 5. Script end
// 3. Promise (microtask)
// 4. queueMicrotask
// 2. setTimeout (macrotask)
Handling Long-Running Tasks
function processLargeArray(arr, chunkSize = 1000) {
let index = 0;
function processChunk() {
const chunk = arr.slice(index, index + chunkSize);
chunk.forEach(item => heavyComputation(item));
index += chunkSize;
if (index < arr.length) {
setTimeout(processChunk, 0); // Yield to event loop
}
}
processChunk();
}
Explanation
The event loop operates in phases:
- Call Stack: Executes synchronous code. When empty, the event loop checks queues.
- Microtask Queue: Processes Promise callbacks, queueMicrotask, and MutationObserver callbacks. Cleared entirely before next macrotask.
- Macrotask Queue: Processes setTimeout, setInterval, setImmediate (Node.js), and I/O events.
- Render Phase: Browsers may update the DOM and repaint if time allows.
Critical rule: All microtasks execute before the next macrotask. This can starve the macrotask queue if microtasks recursively enqueue more microtasks.
Variants
| Runtime | Macrotask API | Microtask API | Notes |
|---|---|---|---|
| Browser | setTimeout, requestAnimationFrame | Promise, queueMicrotask | rAF runs before paint |
| Node.js | setTimeout, setImmediate | Promise, process.nextTick | nextTick runs before Promises |
| Deno | setTimeout | Promise, queueMicrotask | Aligns with browser behavior |
Best Practices
- Break heavy work into chunks: Use setTimeout or requestIdleCallback to yield control
- Prefer microtasks for DOM updates: queueMicrotask ensures DOM reads are batched
- Avoid recursive microtask enqueuing: Can freeze the event loop indefinitely
- Use requestAnimationFrame for visual updates: Synchronizes with the browser’s render cycle
- Profile with Performance tab: Chrome DevTools visualizes microtask and macrotask timing
Common Mistakes
- Assuming setTimeout(0) is immediate: It is always slower than microtasks
- Blocking the main thread: Synchronous loops >50ms cause jank and dropped frames
- Forgetting nextTick in Node.js: process.nextTick runs before Promises, not after
- Mixing microtask recursion: Promise.resolve().then(() => Promise.resolve().then(…)) can deadlock
- Ignoring the render phase: Heavy microtask queues prevent browser painting
Frequently Asked Questions
Q: Why does Promise.then() run before setTimeout(0)? A: Promise callbacks enter the microtask queue, which has higher priority than the macrotask queue where setTimeout callbacks live.
Q: What is the difference between queueMicrotask and Promise.resolve().then()? A: Functionally identical in most cases, but queueMicrotask is more explicit and slightly more efficient.
Q: How do I prevent the event loop from freezing? A: Break work into small chunks using setTimeout, requestIdleCallback, or Web Workers for CPU-intensive tasks.
Related Resources
Prevent Race Conditions in JavaScript Async Code
Identify and fix race conditions in asynchronous JavaScript using proper sequencing, atomic operations, locks, and Promise patterns for predictable concurrent execution
RecipeDeep Clone Objects in JavaScript: Beyond JSON.parse
Compare deep clone strategies including JSON.parse, structuredClone, manual recursion, and library approaches for copying nested objects with circular references and special types
RecipeURL Encoding and Decoding: encodeURI, encodeURIComponent, and Beyond
Master URL encoding in JavaScript and other languages with encodeURI, encodeURIComponent, plus-safe handling, RFC 3986 compliance, and decoding edge cases
RecipeEnable Brotli Compression in Nginx for Faster Asset Delivery
How to configure Brotli compression in Nginx to reduce transfer sizes for JavaScript, CSS, and HTML assets with better ratios than Gzip
RecipeSPA Performance: Code Splitting and Lazy Loading
Improve single-page application load times by splitting bundles at route and component level, implementing lazy loading with React.lazy and dynamic imports