The event loop is the mechanism that allows JavaScript to perform non-blocking operations. This guide explains how it works in detail.
What is the Event Loop? #
JavaScript is single-threaded but can handle asynchronous operations through the event loop. It coordinates execution between the call stack, task queues, and Web APIs.
Call Stack #
The call stack tracks function execution.
function first() {
console.log("First");
second();
console.log("First again");
}
function second() {
console.log("Second");
}
first();
// Call Stack Timeline:
// 1. first() added
// 2. console.log("First") executed
// 3. second() added
// 4. console.log("Second") executed
// 5. second() removed
// 6. console.log("First again") executed
// 7. first() removed
// Output:
// First
// Second
// First again
Web APIs #
Browser provides APIs for async operations:
setTimeout,setInterval- DOM events
fetch,XMLHttpRequest- Promises
console.log("Start");
setTimeout(() => {
console.log("Timeout");
}, 0);
console.log("End");
// Output:
// Start
// End
// Timeout
Task Queues #
Macrotask Queue (Task Queue) #
Contains: setTimeout, setInterval, I/O, UI rendering
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
console.log("3");
// Output: 1, 3, 2
Microtask Queue #
Contains: Promises, queueMicrotask, MutationObserver
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
// Output: 1, 4, 3, 2
// Microtasks run before macrotasks
Event Loop Execution Order #
- Execute synchronous code (call stack)
- Execute all microtasks
- Execute one macrotask
- Repeat from step 2
console.log("1"); // Synchronous
setTimeout(() => {
console.log("2"); // Macrotask
}, 0);
Promise.resolve()
.then(() => console.log("3")) // Microtask
.then(() => console.log("4")); // Microtask
setTimeout(() => {
console.log("5"); // Macrotask
}, 0);
console.log("6"); // Synchronous
// Output: 1, 6, 3, 4, 2, 5
Complex Example #
console.log("Start");
setTimeout(() => {
console.log("setTimeout 1");
Promise.resolve().then(() => console.log("Promise in setTimeout"));
}, 0);
Promise.resolve()
.then(() => {
console.log("Promise 1");
setTimeout(() => console.log("setTimeout in Promise"), 0);
})
.then(() => console.log("Promise 2"));
setTimeout(() => {
console.log("setTimeout 2");
}, 0);
console.log("End");
// Output:
// Start
// End
// Promise 1
// Promise 2
// setTimeout 1
// Promise in setTimeout
// setTimeout 2
// setTimeout in Promise
Async/Await and Event Loop #
async function example() {
console.log("1");
await Promise.resolve();
console.log("2");
}
console.log("3");
example();
console.log("4");
// Output: 3, 1, 4, 2
// await pauses function, continues after current sync code
SetTimeout vs SetImmediate vs Process.nextTick #
In Node.js: #
setTimeout(() => console.log("setTimeout"), 0);
setImmediate(() => console.log("setImmediate"));
process.nextTick(() => console.log("nextTick"));
console.log("sync");
// Output:
// sync
// nextTick
// setTimeout or setImmediate (order varies)
Priority (Node.js): #
- process.nextTick (highest)
- Promises/microtasks
- setTimeout/setInterval
- setImmediate
- I/O callbacks
RequestAnimationFrame #
Runs before next repaint (browser-only):
console.log("1");
requestAnimationFrame(() => {
console.log("2 - RAF");
});
setTimeout(() => {
console.log("3 - setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("4 - Promise");
});
console.log("5");
// Output: 1, 5, 4, 2, 3
Common Patterns #
Avoiding Stack Overflow #
// Synchronous - causes stack overflow
function processArray(items) {
items.forEach(item => {
processItem(item);
});
}
// Asynchronous - spreads work across event loop
async function processArray(items) {
for (const item of items) {
await processItem(item);
await new Promise(resolve => setTimeout(resolve, 0));
}
}Breaking Up Long Tasks #
function heavyComputation(data) {
return new Promise(resolve => {
const batchSize = 1000;
let index = 0;
function processBatch() {
const end = Math.min(index + batchSize, data.length);
for (let i = index; i < end; i++) {
// Process data[i]
}
index = end;
if (index < data.length) {
setTimeout(processBatch, 0);
} else {
resolve();
}
}
processBatch();
});
}Debouncing with Event Loop #
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
const search = debounce(query => {
console.log("Searching:", query);
}, 300);Browser vs Node.js Event Loop #
Browser: #
- Render steps between macrotasks
- requestAnimationFrame before render
- MutationObserver in microtask queue
Node.js: #
- Multiple phases (timers, I/O callbacks, idle, poll, check, close)
- process.nextTick has highest priority
- setImmediate in check phase
Debugging Event Loop Issues #
Visualizing with Timers #
console.time("Task");
setTimeout(() => {
console.timeEnd("Task");
}, 100);
// Shows actual time taken
Finding Blocking Code #
const start = Date.now();
// Blocking operation
for (let i = 0; i < 1000000000; i++) {}
console.log("Blocked for:", Date.now() - start, "ms");Using Performance API #
performance.mark("start");
setTimeout(() => {
performance.mark("end");
performance.measure("task", "start", "end");
const measure = performance.getEntriesByName("task")[0];
console.log("Duration:", measure.duration);
}, 100);Common Pitfalls #
Pitfall 1: Assuming Immediate Execution #
// Wrong assumption
setTimeout(() => {
console.log("Runs immediately");
}, 0); // Actually waits for event loop
console.log("First");
// Output: First, Runs immediately
Pitfall 2: Promise Constructor Executor #
new Promise((resolve) => {
console.log("1"); // Runs immediately!
resolve();
}).then(() => {
console.log("2");
});
console.log("3");
// Output: 1, 3, 2
Pitfall 3: Mixing Callbacks and Promises #
function getData(callback) {
setTimeout(() => {
callback("data");
}, 0);
}
// Callback runs after promise
Promise.resolve().then(() => console.log("Promise"));
getData(data => console.log(data));
// Output: Promise, data
Real-World Examples #
Implementing Sleep #
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function example() {
console.log("Start");
await sleep(1000);
console.log("After 1 second");
}Rate Limiting with Event Loop #
async function rateLimitedFetch(urls, requestsPerSecond) {
const delay = 1000 / requestsPerSecond;
const results = [];
for (const url of urls) {
const result = await fetch(url);
results.push(result);
await new Promise(resolve => setTimeout(resolve, delay));
}
return results;
}Chunked Array Processing #
async function processLargeArray(array, processor) {
const chunkSize = 100;
for (let i = 0; i < array.length; i += chunkSize) {
const chunk = array.slice(i, i + chunkSize);
for (const item of chunk) {
processor(item);
}
// Yield to event loop
await new Promise(resolve => setTimeout(resolve, 0));
}
}Interview Questions #
Q: What will this code output?
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Answer: 1, 4, 3, 2
Q: Why does this happen?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Output: 3, 3, 3
// Because var is function-scoped, all callbacks share same 'i'
Q: How to fix it?
// Solution 1: let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Solution 2: IIFE
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 0);
})(i);
}Best Practices #
- Don’t block the event loop - Break up long tasks
- Understand microtask priority - Promises before setTimeout
- Use async/await for cleaner async code
- Avoid nested callbacks - Use promises or async/await
- Monitor performance - Profile blocking operations
- Test async behavior - Don’t assume execution order
Understanding the event loop is crucial for writing efficient JavaScript. It explains why async code behaves the way it does and helps you write better concurrent programs.