# JavaScript Event Loop Explained: Understanding Async Execution
Deep dive into call stack, task queues, microtasks and macrotasks execution order
The event loop is the core of JavaScript asynchronous programming. This article explores how it works in depth.
Basic Concepts
Runtime Model
JavaScript Runtime Structure:
┌─────────────────────────────────────────────────────┐
│ │
│ Heap │
│ └── Memory region for object allocation │
│ │
│ Call Stack │
│ └── Execution context for function calls │
│ │
│ Web APIs │
│ ├── setTimeout / setInterval │
│ ├── fetch / XMLHttpRequest │
│ ├── DOM event listeners │
│ └── requestAnimationFrame │
│ │
│ Task Queues │
│ ├── Macrotask Queue │
│ └── Microtask Queue │
│ │
└─────────────────────────────────────────────────────┘
Call Stack
function first() {
console.log('First');
second();
console.log('First End');
}
function second() {
console.log('Second');
third();
console.log('Second End');
}
function third() {
console.log('Third');
}
first();
// Call stack changes:
// 1. first() pushed
// 2. console.log('First') pushed, runs, popped
// 3. second() pushed
// 4. console.log('Second') pushed, runs, popped
// 5. third() pushed
// 6. console.log('Third') pushed, runs, popped
// 7. third() popped
// 8. console.log('Second End') pushed, runs, popped
// 9. second() popped
// 10. console.log('First End') pushed, runs, popped
// 11. first() popped
Macrotasks and Microtasks
Task Types
Task Classification:
┌─────────────────────────────────────────────────────┐
│ │
│ Macrotasks │
│ ├── script (overall code) │
│ ├── setTimeout / setInterval │
│ ├── setImmediate (Node.js) │
│ ├── I/O │
│ ├── UI rendering │
│ └── requestAnimationFrame │
│ │
│ Microtasks │
│ ├── Promise.then / catch / finally │
│ ├── async/await │
│ ├── queueMicrotask() │
│ ├── MutationObserver │
│ └── process.nextTick (Node.js) │
│ │
└─────────────────────────────────────────────────────┘
Execution Order
console.log('1: Script Start');
setTimeout(() => {
console.log('2: setTimeout');
}, 0);
Promise.resolve()
.then(() => console.log('3: Promise 1'))
.then(() => console.log('4: Promise 2'));
queueMicrotask(() => {
console.log('5: queueMicrotask');
});
console.log('6: Script End');
// Output order:
// 1: Script Start
// 6: Script End
// 3: Promise 1
// 5: queueMicrotask
// 4: Promise 2
// 2: setTimeout
// Explanation:
// 1. Synchronous code runs first (1, 6)
// 2. Microtask queue emptied (3, 5, 4)
// 3. Macrotask runs (2)
Event Loop Flow
// One iteration of the event loop
while (true) {
// 1. Get oldest task from macrotask queue
const macrotask = macrotaskQueue.shift();
if (macrotask) {
runTask(macrotask);
}
// 2. Run all microtasks
while (microtaskQueue.length > 0) {
const microtask = microtaskQueue.shift();
runTask(microtask);
}
// 3. Render if needed
if (needsRender()) {
// Run requestAnimationFrame callbacks
runAnimationFrames();
// Render
render();
}
}
Classic Interview Questions
Question One
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
}, 0);
Promise.resolve().then(() => {
console.log('4');
setTimeout(() => console.log('5'), 0);
});
console.log('6');
// Output: 1, 6, 4, 2, 3, 5
Question Two
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
async1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
// Output:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
Question Three
Promise.resolve()
.then(() => {
console.log('promise1');
return Promise.resolve('promise2');
})
.then((res) => {
console.log(res);
});
Promise.resolve()
.then(() => {
console.log('promise3');
})
.then(() => {
console.log('promise4');
})
.then(() => {
console.log('promise5');
});
// Output: promise1, promise3, promise4, promise2, promise5
// Note: return Promise.resolve() creates extra microtasks
Node.js Event Loop
Node.js Phases
Node.js Event Loop Phases:
┌─────────────────────────────────────────────────────┐
│ │
│ ┌───────────────────────────────┐ │
│ │ timers │ │
│ │ setTimeout / setInterval │ │
│ └───────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────┐ │
│ │ pending callbacks │ │
│ │ System operation callbacks │ │
│ └───────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────┐ │
│ │ idle, prepare │ │
│ │ Internal use │ │
│ └───────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────┐ │
│ │ poll │ │
│ │ I/O callbacks, new events │ │
│ └───────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────┐ │
│ │ check │ │
│ │ setImmediate callbacks │ │
│ └───────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────┐ │
│ │ close callbacks │ │
│ │ Close handlers │ │
│ └───────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
setTimeout vs setImmediate
// In I/O callbacks, setImmediate always runs first
const fs = require('fs');
fs.readFile('file.txt', () => {
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
});
// Output: immediate, timeout (stable in I/O callback)
// In main module, order is non-deterministic
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
// Could be timeout, immediate OR immediate, timeout
process.nextTick
// process.nextTick has highest priority
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);
// Output: nextTick, promise, timeout, immediate
// Warning: Too many nextTick calls can block I/O
function recursive() {
process.nextTick(recursive); // Dangerous! Blocks event loop
}
Practical Applications
Avoiding Blocking
// Blocking example
function processLargeArray(array) {
array.forEach(item => {
// Time-consuming operation
heavyComputation(item);
});
}
// Non-blocking improvement
async function processLargeArrayAsync(array) {
for (let i = 0; i < array.length; i++) {
heavyComputation(array[i]);
// Yield control every 100 items
if (i % 100 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
// Using requestIdleCallback (browser)
function processWhenIdle(items) {
let index = 0;
function process(deadline) {
while (index < items.length && deadline.timeRemaining() > 0) {
heavyComputation(items[index]);
index++;
}
if (index < items.length) {
requestIdleCallback(process);
}
}
requestIdleCallback(process);
}
Batched DOM Updates
// Using microtasks for batched updates
let pending = false;
const updates = [];
function scheduleUpdate(update) {
updates.push(update);
if (!pending) {
pending = true;
queueMicrotask(() => {
pending = false;
flushUpdates();
});
}
}
function flushUpdates() {
const batch = updates.splice(0);
batch.forEach(update => update());
}
// Using requestAnimationFrame
let rafPending = false;
const rafUpdates = [];
function scheduleRafUpdate(update) {
rafUpdates.push(update);
if (!rafPending) {
rafPending = true;
requestAnimationFrame(() => {
rafPending = false;
rafUpdates.splice(0).forEach(fn => fn());
});
}
}
Best Practices Summary
Event Loop Best Practices:
┌─────────────────────────────────────────────────────┐
│ │
│ Avoid Blocking │
│ ├── Break long tasks into chunks │
│ ├── Use Web Workers │
│ ├── Avoid synchronous blocking operations │
│ └── Use requestIdleCallback │
│ │
│ Task Scheduling │
│ ├── Understand micro vs macro task difference │
│ ├── Use setTimeout(fn, 0) appropriately │
│ ├── Use requestAnimationFrame for animations │
│ └── Use microtasks for batched updates │
│ │
│ Performance │
│ ├── Monitor long tasks │
│ ├── Avoid deep Promise chains │
│ └── Watch for nextTick/queueMicrotask abuse │
│ │
└─────────────────────────────────────────────────────┘
| Task Type | When Executed |
|---|---|
| Synchronous | Immediately |
| Microtask | After current macrotask |
| Macrotask | Next event loop iteration |
| requestAnimationFrame | Before render |
Understanding the event loop is key to mastering JavaScript async programming.