JavaScript Core
Event Loop
How JavaScript handles asynchronous operations
Advanced
javascript async concurrency performance
Definition
The event loop is JavaScript’s mechanism for handling asynchronous operations. It is what allows JavaScript to perform non-blocking I/O operations despite being single-threaded, by offloading operations to the system kernel whenever possible and managing callbacks through queues.
The Runtime Components
Call Stack
// Synchronous execution
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
const squared = square(n);
console.log(squared);
}
printSquare(4);
// Execution order:
// 1. printSquare enters stack
// 2. square enters stack
// 3. multiply enters stack
// 4. multiply returns 16, exits
// 5. square returns 16, exits
// 6. console.log prints 16
// 7. printSquare exits
Web APIs (Browser Environment)
// These are provided by the browser, not V8
setTimeout(callback, delay);
fetch(url);
XMLHttpRequest;
DOM events;
requestAnimationFrame;
Task Queues
// Macrotask Queue
setTimeout(() => console.log('timeout'), 0);
setInterval(() => console.log('interval'), 1000);
setImmediate(() => console.log('immediate')); // Node.js
I/O operations;
UI rendering;
// Microtask Queue (Higher priority)
Promise.then/catch/finally;
queueMicrotask(callback);
MutationObserver;
process.nextTick; // Node.js
How the Event Loop Works
┌─────────────────────────┐
│ Call Stack │
│ (synchronous code) │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Web APIs │
│ (async operations) │
└───────────┬─────────────┘
│
┌───────┴───────┐
▼ ▼
┌──────────┐ ┌─────────────┐
│Microtask │ │ Macrotask │
│ Queue │ │ Queue │
└────┬─────┘ └──────┬──────┘
│ │
└───────┬───────┘
│
▼
┌─────────────────┐
│ Event Loop │
│ (checks queues) │
└─────────────────┘
Execution Order Example
console.log('1'); // Synchronous
setTimeout(() => {
console.log('2'); // Macrotask
}, 0);
Promise.resolve().then(() => {
console.log('3'); // Microtask
});
console.log('4'); // Synchronous
// Output: 1, 4, 3, 2
// Explanation:
// 1. '1' and '4' execute immediately (call stack)
// 2. Promise.then goes to microtask queue
// 3. setTimeout goes to macrotask queue
// 4. Call stack empty -> process microtasks -> '3'
// 5. Microtasks empty -> process macrotasks -> '2'
Detailed Execution Flow
Complex Example
console.log('Script start');
setTimeout(() => {
console.log('setTimeout 1');
Promise.resolve().then(() => {
console.log('Promise inside timeout');
});
}, 0);
setTimeout(() => {
console.log('setTimeout 2');
}, 0);
Promise.resolve().then(() => {
console.log('Promise 1');
Promise.resolve().then(() => {
console.log('Promise 2');
});
});
Promise.resolve().then(() => {
console.log('Promise 3');
});
console.log('Script end');
// Output:
// Script start
// Script end
// Promise 1
// Promise 2
// Promise 3
// setTimeout 1
// Promise inside timeout
// setTimeout 2
Microtasks vs Macrotasks
Priority Order
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
Promise.resolve().then(() => console.log('4'));
setTimeout(() => console.log('5'), 0);
// Output: 1, 3, 4, 2, 5
// Microtasks execute before next macrotask
Queue Processing
// All microtasks execute before any macrotask
Promise.resolve()
.then(() => console.log('microtask 1'))
.then(() => console.log('microtask 2'))
.then(() => console.log('microtask 3'));
setTimeout(() => console.log('macrotask'), 0);
// Output: microtask 1, 2, 3, then macrotask
Practical Implications
Blocking the Event Loop
// Never do this
function blockFor(seconds) {
const start = Date.now();
while (Date.now() - start < seconds * 1000) {
// Blocks everything!
}
}
// UI freezes, no updates, no events processed
blockFor(5);
Yielding to Event Loop
// Break up heavy work
async function processLargeArray(items) {
const chunkSize = 100;
for (let i = 0; i < items.length; i += chunkSize) {
const chunk = items.slice(i, i + chunkSize);
processChunk(chunk);
// Yield to event loop
await new Promise(resolve => setTimeout(resolve, 0));
}
}
requestAnimationFrame
// Syncs with browser's repaint cycle
function smoothAnimation() {
let start;
function step(timestamp) {
if (!start) start = timestamp;
const progress = timestamp - start;
// Update animation
element.style.transform = `translateX(${progress / 10}px)`;
if (progress < 1000) {
requestAnimationFrame(step);
}
}
requestAnimationFrame(step);
}
setTimeout(fn, 0) Explained
console.log('A');
setTimeout(() => {
console.log('B');
}, 0);
Promise.resolve().then(() => {
console.log('C');
});
console.log('D');
// Output: A, D, C, B
// Even with 0ms delay, setTimeout is a macrotask
// and executes after all microtasks
Node.js Event Loop
Phases Overview
// Node.js has additional phases
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌─────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections│
│ └─────────────┬─────────────┘ │ data, etc.│
│ ┌─────────────┴─────────────┐ └─────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
process.nextTick
// Highest priority - executes before any other phase
console.log('1');
process.nextTick(() => {
console.log('nextTick 1');
process.nextTick(() => console.log('nextTick 2'));
});
Promise.resolve().then(() => console.log('Promise'));
setTimeout(() => console.log('setTimeout'), 0);
console.log('2');
// Output: 1, 2, nextTick 1, nextTick 2, Promise, setTimeout
Common Pitfalls
MutationObserver Timing
// MutationObserver is a microtask
const observer = new MutationObserver(() => {
console.log('MutationObserver');
});
observer.observe(document.body, { childList: true });
document.body.appendChild(document.createElement('div'));
Promise.resolve().then(() => console.log('Promise'));
// Output order can vary by browser implementation!
Promise.resolve() vs new Promise
// These behave differently!
Promise.resolve().then(() => console.log('1'));
new Promise(resolve => {
console.log('2');
resolve();
}).then(() => console.log('3'));
// Output: 2, 1, 3
// Promise constructor runs synchronously
Performance Considerations
Task Budgeting
// Monitor frame time to avoid jank
function scheduleWork(workFn) {
const start = performance.now();
requestAnimationFrame(() => {
const frameTime = performance.now() - start;
if (frameTime > 16) { // Missed 60fps budget
setTimeout(workFn, 0); // Defer to next frame
} else {
workFn();
}
});
}
Async Iterator Pattern
async function* asyncGenerator() {
for (let i = 0; i < 10; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
yield i;
}
}
// Automatically yields between iterations
for await (const value of asyncGenerator()) {
console.log(value);
}
Key Takeaway
The event loop enables JavaScript’s non-blocking nature. Remember: call stack executes first, then all microtasks (Promises), then a single macrotask (setTimeout, etc.). Understanding execution order is crucial for debugging async code, avoiding race conditions, and writing performant applications that do not block the main thread.