Event Loop and Microtasks in JavaScript
Introduction
The event loop is the heart of JavaScript’s asynchronous execution model. Understanding how the event loop works, along with microtasks and macrotasks, is essential for writing efficient asynchronous code and debugging timing-related issues. In this article, you’ll learn how JavaScript executes code, manages different types of tasks, and optimizes performance.
Understanding the Event Loop
What is the Event Loop?
The event loop is a mechanism that allows JavaScript to perform non-blocking operations despite being single-threaded. It continuously checks for tasks to execute and manages the execution order.
// Simple example showing event loop behavior
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
// Output:
// Start
// End
// Timeout
The Call Stack
// Call stack example
function first() {
console.log('First');
second();
}
function second() {
console.log('Second');
third();
}
function third() {
console.log('Third');
}
first();
// Output:
// First
// Second
// Third
// Call stack visualization:
// 1. first() pushed to stack
// 2. second() pushed to stack
// 3. third() pushed to stack
// 4. third() completes, popped from stack
// 5. second() completes, popped from stack
// 6. first() completes, popped from stack
Microtasks vs Macrotasks
Understanding the Difference
// Microtasks: Promises, MutationObserver, queueMicrotask()
// Macrotasks: setTimeout, setInterval, setImmediate, I/O
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
})
.then(() => {
console.log('Promise 2');
});
console.log('Script end');
// Output:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout
Microtask Queue
// Microtasks execute before macrotasks
console.log('Start');
// Macrotask
setTimeout(() => {
console.log('Macrotask 1');
}, 0);
// Microtask
Promise.resolve()
.then(() => {
console.log('Microtask 1');
});
// Macrotask
setTimeout(() => {
console.log('Macrotask 2');
}, 0);
// Microtask
Promise.resolve()
.then(() => {
console.log('Microtask 2');
});
console.log('End');
// Output:
// Start
// End
// Microtask 1
// Microtask 2
// Macrotask 1
// Macrotask 2
Macrotask Queue
// Macrotasks are processed one at a time
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
Promise.resolve().then(() => console.log('Promise in Timeout 1'));
}, 0);
setTimeout(() => {
console.log('Timeout 2');
}, 0);
console.log('End');
// Output:
// Start
// End
// Timeout 1
// Promise in Timeout 1
// Timeout 2
Event Loop Execution Order
Complete Execution Flow
// 1. Execute all synchronous code (call stack)
// 2. Execute all microtasks (promises, queueMicrotask)
// 3. Render (if needed)
// 4. Execute one macrotask (setTimeout, setInterval)
// 5. Go back to step 2
console.log('1. Synchronous');
setTimeout(() => {
console.log('2. Macrotask (setTimeout)');
Promise.resolve().then(() => console.log('3. Microtask in Macrotask'));
}, 0);
Promise.resolve()
.then(() => {
console.log('4. Microtask (Promise)');
setTimeout(() => {
console.log('5. Macrotask in Microtask');
}, 0);
});
console.log('6. Synchronous');
// Output:
// 1. Synchronous
// 6. Synchronous
// 4. Microtask (Promise)
// 2. Macrotask (setTimeout)
// 3. Microtask in Macrotask
// 5. Macrotask in Microtask
Microtasks in Detail
Promise Microtasks
// Promises create microtasks
console.log('Start');
Promise.resolve()
.then(() => console.log('Promise 1'))
.then(() => console.log('Promise 2'))
.then(() => console.log('Promise 3'));
console.log('End');
// Output:
// Start
// End
// Promise 1
// Promise 2
// Promise 3
queueMicrotask()
// queueMicrotask() explicitly queues a microtask
console.log('Start');
queueMicrotask(() => {
console.log('Microtask 1');
});
Promise.resolve().then(() => {
console.log('Microtask 2');
});
queueMicrotask(() => {
console.log('Microtask 3');
});
console.log('End');
// Output:
// Start
// End
// Microtask 1
// Microtask 2
// Microtask 3
MutationObserver
// MutationObserver creates microtasks
console.log('Start');
const observer = new MutationObserver(() => {
console.log('DOM changed');
});
const element = document.createElement('div');
observer.observe(element, { attributes: true });
element.setAttribute('data-test', 'value');
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
// Output:
// Start
// End
// DOM changed
// Promise
Macrotasks in Detail
setTimeout and setInterval
// setTimeout creates macrotasks
console.log('Start');
setTimeout(() => {
console.log('Timeout 1');
}, 0);
setTimeout(() => {
console.log('Timeout 2');
}, 10);
console.log('End');
// Output:
// Start
// End
// Timeout 1
// Timeout 2
setImmediate (Node.js)
// setImmediate creates macrotasks (Node.js only)
console.log('Start');
setImmediate(() => {
console.log('Immediate 1');
});
Promise.resolve().then(() => {
console.log('Promise');
});
setImmediate(() => {
console.log('Immediate 2');
});
console.log('End');
// Output (Node.js):
// Start
// End
// Promise
// Immediate 1
// Immediate 2
Practical Event Loop Examples
Example 1: Complex Execution Order
// Complex example showing event loop behavior
console.log('Script start');
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve().then(() => console.log('Promise in setTimeout 1'));
}, 0);
Promise.resolve()
.then(() => {
console.log('Promise 1');
setTimeout(() => console.log('setTimeout in Promise 1'), 0);
})
.then(() => {
console.log('Promise 2');
});
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
console.log('Script end');
// Output:
// Script start
// Script end
// Promise 1
// Promise 2
// setTimeout 1
// Promise in setTimeout 1
// setTimeout 2
// setTimeout in Promise 1
Example 2: Rendering and Event Loop
// Event loop with rendering
console.log('Start');
// This will block the main thread
const start = performance.now();
while (performance.now() - start < 100) {
// Busy wait for 100ms
}
console.log('After blocking');
// Rendering happens here if needed
setTimeout(() => {
console.log('After rendering');
}, 0);
// Output:
// Start
// After blocking
// After rendering
Example 3: Task Starvation
// Microtasks can starve macrotasks
function recursiveMicrotask() {
console.log('Microtask');
Promise.resolve().then(() => {
recursiveMicrotask();
});
}
recursiveMicrotask();
setTimeout(() => {
console.log('This might never run!');
}, 0);
// The setTimeout will be delayed indefinitely
// because microtasks keep the event loop busy
Performance Implications
Microtasks vs Macrotasks Performance
// Microtasks are faster (execute sooner)
console.time('Microtask');
Promise.resolve().then(() => {
console.timeEnd('Microtask');
});
console.time('Macrotask');
setTimeout(() => {
console.timeEnd('Macrotask');
}, 0);
// Microtask is typically faster
Batch Updates with Microtasks
// Use microtasks for batching updates
class BatchUpdater {
constructor() {
this.updates = [];
this.scheduled = false;
}
schedule(update) {
this.updates.push(update);
if (!this.scheduled) {
this.scheduled = true;
Promise.resolve().then(() => {
this.flush();
});
}
}
flush() {
const updates = this.updates;
this.updates = [];
this.scheduled = false;
console.log(`Processing ${updates.length} updates`);
updates.forEach(update => update());
}
}
const updater = new BatchUpdater();
updater.schedule(() => console.log('Update 1'));
updater.schedule(() => console.log('Update 2'));
updater.schedule(() => console.log('Update 3'));
// Output:
// Processing 3 updates
// Update 1
// Update 2
// Update 3
Real-World Examples
Example 1: Debouncing with Event Loop
// Debounce using event loop knowledge
function debounce(func, delay) {
let timeoutId;
let lastArgs;
let microtaskScheduled = false;
return function(...args) {
lastArgs = args;
clearTimeout(timeoutId);
if (!microtaskScheduled) {
microtaskScheduled = true;
Promise.resolve().then(() => {
microtaskScheduled = false;
});
}
timeoutId = setTimeout(() => {
func.apply(this, lastArgs);
}, delay);
};
}
const debouncedLog = debounce(console.log, 100);
debouncedLog('Hello');
debouncedLog('World');
// Only 'World' is logged after 100ms
Example 2: Priority Queue
// Priority queue using microtasks and macrotasks
class PriorityQueue {
constructor() {
this.highPriority = [];
this.normalPriority = [];
}
addHighPriority(task) {
this.highPriority.push(task);
Promise.resolve().then(() => {
this.processHighPriority();
});
}
addNormalPriority(task) {
this.normalPriority.push(task);
setTimeout(() => {
this.processNormalPriority();
}, 0);
}
processHighPriority() {
while (this.highPriority.length > 0) {
const task = this.highPriority.shift();
task();
}
}
processNormalPriority() {
if (this.normalPriority.length > 0) {
const task = this.normalPriority.shift();
task();
}
}
}
const queue = new PriorityQueue();
queue.addNormalPriority(() => console.log('Normal 1'));
queue.addHighPriority(() => console.log('High 1'));
queue.addNormalPriority(() => console.log('Normal 2'));
queue.addHighPriority(() => console.log('High 2'));
// Output:
// High 1
// High 2
// Normal 1
// Normal 2
Example 3: Async Batch Processing
// Batch processing with event loop
class AsyncBatcher {
constructor(batchSize = 10) {
this.batchSize = batchSize;
this.queue = [];
this.processing = false;
}
add(item) {
this.queue.push(item);
if (this.queue.length >= this.batchSize) {
this.processBatch();
} else if (!this.processing) {
this.processing = true;
Promise.resolve().then(() => {
if (this.queue.length > 0) {
this.processBatch();
}
this.processing = false;
});
}
}
processBatch() {
const batch = this.queue.splice(0, this.batchSize);
console.log(`Processing batch of ${batch.length} items`);
batch.forEach(item => {
// Process item
});
}
}
const batcher = new AsyncBatcher(3);
batcher.add('item1');
batcher.add('item2');
batcher.add('item3');
// Processing batch of 3 items
batcher.add('item4');
// Scheduled for next microtask
Debugging Event Loop Issues
Identifying Blocking Code
// Detect blocking code
function measureEventLoopLag() {
let lastTime = performance.now();
setInterval(() => {
const currentTime = performance.now();
const lag = currentTime - lastTime - 1000; // Expected 1000ms
if (lag > 50) {
console.warn(`Event loop lag: ${lag}ms`);
}
lastTime = currentTime;
}, 1000);
}
measureEventLoopLag();
Visualizing Task Execution
// Visualize task execution order
function visualizeEventLoop() {
console.log('=== Event Loop Visualization ===');
console.log('1. Synchronous code');
setTimeout(() => {
console.log('2. Macrotask (setTimeout)');
}, 0);
Promise.resolve().then(() => {
console.log('3. Microtask (Promise)');
});
queueMicrotask(() => {
console.log('4. Microtask (queueMicrotask)');
});
console.log('5. More synchronous code');
}
visualizeEventLoop();
// Output shows execution order clearly
Common Mistakes to Avoid
Mistake 1: Assuming setTimeout(0) is Immediate
// โ Wrong - Assuming setTimeout(0) runs immediately
setTimeout(() => {
console.log('This is not immediate');
}, 0);
// โ
Correct - Use Promise for immediate execution
Promise.resolve().then(() => {
console.log('This is more immediate');
});
Mistake 2: Blocking the Event Loop
// โ Wrong - Blocking the event loop
function blockingLoop() {
const start = performance.now();
while (performance.now() - start < 1000) {
// Busy wait - blocks everything
}
}
// โ
Correct - Use async operations
async function nonBlockingLoop() {
for (let i = 0; i < 100; i++) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
Mistake 3: Microtask Starvation
// โ Wrong - Infinite microtasks starve macrotasks
function infiniteMicrotasks() {
Promise.resolve().then(() => {
console.log('Microtask');
infiniteMicrotasks(); // Creates infinite loop
});
}
// โ
Correct - Limit microtask recursion
function limitedMicrotasks(count = 0) {
if (count < 10) {
Promise.resolve().then(() => {
console.log('Microtask');
limitedMicrotasks(count + 1);
});
}
}
Summary
The event loop is fundamental to JavaScript:
- Event loop manages synchronous and asynchronous code
- Microtasks (Promises) execute before macrotasks (setTimeout)
- Call stack must be empty before processing tasks
- Rendering happens between macrotasks
- Understanding event loop improves performance
- Avoid blocking the main thread
- Use microtasks for high-priority operations
- Use macrotasks for lower-priority operations
- Monitor event loop lag in production
Related Resources
Next Steps
Continue your learning journey:
- Promises: Creation, Chaining, Resolution
- Async/Await: Modern Asynchronous Programming
- Error Handling with Promises and Async/Await
- Promise Utilities: all, race, allSettled, any
Comments