Introduction
JavaScript is single-threaded, yet it handles asynchronous operations beautifully. The Event Loop is the mechanism that makes this possible. Understanding it is crucial for writing efficient JavaScript code.
The Single-Threaded Model
What Does Single-Threaded Mean?
JavaScript executes code one line at a time in a single execution context:
console.log("1");
console.log("2");
console.log("3");
// Always executes in order: 1, 2, 3
But wait⦠How does it handle:
- setTimeout
- fetch requests
- DOM events
- File I/O
Answer: The Event Loop!
Event Loop Architecture
Components
βββββββββββββββββββββββββββββββββββββββ
β Call Stack β
β (Main thread - synchronous code) β
βββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββ
β Web APIs β
β (Browser APIs - async operations) β
β - setTimeout β
β - fetch β
β - DOM events β
βββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββ
β Callback Queue (Task Queue) β
β (Ready callbacks waiting to run) β
βββββββββββββββββββββββββββββββββββββββ
β
β
βββββββββββββββββββββββββββββββββββββββ
β Microtask Queue β
β (Promise callbacks, queueMicrotask)β
βββββββββββββββββββββββββββββββββββββββ
How It Works
Step-by-Step Execution
console.log("1");
setTimeout(() => {
console.log("2");
}, 0);
Promise.resolve().then(() => {
console.log("3");
});
console.log("4");
// Output: 1, 4, 3, 2
Execution flow:
console.log('1')β Call Stack β Executes β Outputs β1βsetTimeout(...)β Call Stack β Web APIs β Timer startsPromise.resolve().then(...)β Call Stack β Microtask Queueconsole.log('4')β Call Stack β Executes β Outputs β4β- Call Stack empty β Check Microtask Queue β Execute Promise β Outputs β3β
- Check Callback Queue β Execute setTimeout β Outputs β2β
Key Rules
- Call Stack must be empty before callbacks run
- Microtasks run before Macrotasks
- Microtasks include: Promises, queueMicrotask, MutationObserver
- Macrotasks include: setTimeout, setInterval, I/O, UI rendering
Call Stack
The call stack is where synchronous code executes:
function a() {
console.log("a");
b();
}
function b() {
console.log("b");
c();
}
function c() {
console.log("c");
}
a();
// Call Stack:
// [a] β [a, b] β [a, b, c] β [a, b] β [a] β []
Stack Overflow
function infinite() {
infinite(); // Recursive call
}
infinite();
// RangeError: Maximum call stack size exceeded
Web APIs
Browser APIs handle asynchronous operations outside the main thread:
// setTimeout - Timer API
setTimeout(() => {
console.log("After 1 second");
}, 1000);
// fetch - Network API
fetch("/api/data")
.then((res) => res.json())
.then((data) => console.log(data));
// addEventListener - DOM API
button.addEventListener("click", () => {
console.log("Clicked!");
});
These APIs run in separate threads, then queue callbacks when complete.
Callback Queue (Task Queue)
Also called the βMacrotask Queueβ - handles:
- setTimeout/setInterval callbacks
- I/O operations
- UI rendering
console.log("Start");
setTimeout(() => {
console.log("Timeout 1");
}, 0);
setTimeout(() => {
console.log("Timeout 2");
}, 0);
console.log("End");
// Output: Start, End, Timeout 1, Timeout 2
Microtask Queue
Higher priority than callback queue - handles:
- Promise callbacks (.then, .catch, .finally)
- queueMicrotask()
- MutationObserver
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
queueMicrotask(() => console.log("4"));
console.log("5");
// Output: 1, 5, 3, 4, 2
// Microtasks (3, 4) run before macrotask (2)
Promises and the Event Loop
Promise Execution
console.log("Start");
Promise.resolve()
.then(() => console.log("Promise 1"))
.then(() => console.log("Promise 2"));
setTimeout(() => {
console.log("Timeout");
}, 0);
Promise.resolve().then(() => console.log("Promise 3"));
console.log("End");
// Output:
// Start
// End
// Promise 1
// Promise 2
// Promise 3
// Timeout
Why? All Promise callbacks go to Microtask Queue and execute before Callback Queue.
Async/Await
Async/await is syntactic sugar over Promises:
async function example() {
console.log("1");
await Promise.resolve();
console.log("2");
await Promise.resolve();
console.log("3");
}
console.log("Start");
example();
console.log("End");
// Output: Start, 1, End, 2, 3
Behind the scenes:
awaitpauses function execution- Rest of function becomes Promise callback
- Goes to Microtask Queue
Common Patterns
Blocking the Event Loop
// β Bad - blocks event loop
function heavyComputation() {
for (let i = 0; i < 1000000000; i++) {
// Long computation
}
}
heavyComputation();
// Browser freezes - can't process events!
// β
Good - yields to event loop
async function heavyComputation() {
for (let i = 0; i < 1000000000; i++) {
if (i % 1000000 === 0) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
}
Race Conditions
let value = 0;
// Both modify value
setTimeout(() => value++, 0);
setTimeout(() => value++, 0);
// Result depends on timing!
Best Practices
β Do This
1. Use async/await for readability
async function fetchData() {
const response = await fetch("/api/data");
const data = await response.json();
return data;
}
2. Break up long computations
async function processLargeArray(arr) {
for (let i = 0; i < arr.length; i += 1000) {
processChunk(arr.slice(i, i + 1000));
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
3. Use requestAnimationFrame for animations
function animate() {
// Update animation
requestAnimationFrame(animate);
}
animate();
β Donβt Do This
1. Donβt block the event loop
// β Blocks everything
while (true) {
// Infinite loop
}
// β Heavy synchronous computation
for (let i = 0; i < 1e9; i++) {
// Long calculation
}
2. Donβt create callback hell
// β Nested callbacks
getData((data) => {
processData(data, (result) => {
saveData(result, (saved) => {
// Too nested!
});
});
});
// β
Use async/await
async function workflow() {
const data = await getData();
const result = await processData(data);
await saveData(result);
}
Visualizing the Event Loop
Use our tools:
- Event Loop Visualizer - See it in action
- Promise Visualizer - Understand promise chains
Conclusion
The Event Loop is fundamental to JavaScript:
Key takeaways:
- JavaScript is single-threaded
- Event Loop enables async operations
- Microtasks run before macrotasks
- Donβt block the event loop
- Use async/await for cleaner code
Understanding the Event Loop helps you:
- Debug async issues
- Write performant code
- Avoid blocking the UI
- Predict execution order
Next Steps
- Try Event Loop Visualizer
- Learn about Promises
- Explore Async Patterns