JavaScript Event Loop
JavaScript is a single-threaded language — it can only execute one piece of code at a time. Yet it handles timers, network requests, and user events without freezing. This is made possible by the Event Loop, one of the most important concepts in JavaScript.
Understanding the event loop explains why async code runs the way it does — and why code sometimes runs in an unexpected order.
JavaScript Runtime Components
To understand the event loop, the key components of the JavaScript runtime must first be known:
| Component | Role |
|---|---|
| Call Stack | Tracks which function is currently executing |
| Heap | Memory where objects and variables are stored |
| Web APIs | Browser-provided features: setTimeout, fetch, DOM events |
| Callback Queue (Task Queue) | Stores callbacks from Web APIs waiting to run |
| Microtask Queue | Stores Promise callbacks — higher priority than Callback Queue |
| Event Loop | Monitors the call stack and moves tasks from queues to the stack |
The Call Stack
The call stack is a LIFO (Last In, First Out) structure that keeps track of function calls. When a function is called, it is pushed onto the stack. When it returns, it is popped off.
function greet(name) {
return "Hello, " + name;
}
function showGreeting() {
let msg = greet("Priya");
console.log(msg);
}
showGreeting();
// Call Stack Order:
// 1. showGreeting() pushed
// 2. greet("Priya") pushed (called inside showGreeting)
// 3. greet() returns — popped
// 4. console.log() called — popped
// 5. showGreeting() returns — popped
// Stack is emptyStack Overflow
If the call stack grows too deep (e.g., infinite recursion), it causes a stack overflow error.
function infinite() {
infinite(); // Calls itself forever
}
// infinite(); // Uncaught RangeError: Maximum call stack size exceededHow Async Code Works with Web APIs
When an async operation like setTimeout or fetch is called, JavaScript hands it off to the browser's Web API. The Web API handles the timer or network call independently. When complete, the callback is placed in the Callback Queue, waiting for the call stack to be empty.
console.log("1 - Start");
setTimeout(() => {
console.log("3 - Inside timeout");
}, 0);
console.log("2 - End");
// Output:
// 1 - Start
// 2 - End
// 3 - Inside timeout
// setTimeout runs AFTER synchronous code even with 0ms delayThe Event Loop in Action
The Event Loop continuously checks: Is the call stack empty? If yes, move the next item from the queue to the stack and execute it.
console.log("Step 1"); // Synchronous
setTimeout(() => {
console.log("Step 3"); // Macrotask (Callback Queue)
}, 0);
Promise.resolve().then(() => {
console.log("Step 2"); // Microtask (Microtask Queue — runs before macrotask)
});
console.log("Step 4 (before resolve runs)");
// Output:
// Step 1
// Step 4 (before resolve runs)
// Step 2 <-- Promise microtask runs BEFORE setTimeout
// Step 3 <-- setTimeout macrotask runs lastMicrotasks vs Macrotasks
JavaScript has two task queues with different priorities:
| Queue | Sources | Priority |
|---|---|---|
| Microtask Queue | Promise callbacks (.then, .catch, .finally), queueMicrotask() | Higher — runs before macrotasks |
| Macrotask Queue (Callback Queue) | setTimeout, setInterval, UI events, I/O | Lower — runs after microtasks are empty |
After each macrotask, the entire microtask queue is drained before the next macrotask runs.
setTimeout(() => console.log("Macrotask 1"), 0);
setTimeout(() => console.log("Macrotask 2"), 0);
Promise.resolve().then(() => console.log("Microtask 1"));
Promise.resolve().then(() => console.log("Microtask 2"));
console.log("Synchronous");
// Output:
// Synchronous
// Microtask 1
// Microtask 2
// Macrotask 1
// Macrotask 2Async/Await and the Event Loop
Behind the scenes, await is syntactic sugar for a .then() callback. When a function hits await, it suspends and allows other code to run. The resumption is scheduled as a microtask.
async function fetchAndLog() {
console.log("Before await");
let result = await Promise.resolve("Data loaded");
console.log("After await:", result);
}
fetchAndLog();
console.log("After function call");
// Output:
// Before await
// After function call <-- runs while fetchAndLog is suspended
// After await: Data loaded <-- resumes as microtaskNon-Blocking JavaScript
Because of the event loop, JavaScript can start long-running operations (like fetching data) and continue executing other code. The callback only runs when the operation completes and the stack is free.
console.log("Sending request...");
fetch("https://jsonplaceholder.typicode.com/posts/1")
.then(res => res.json())
.then(data => {
console.log("Received:", data.title);
});
console.log("Request sent. Continuing with other tasks...");
// Output:
// Sending request...
// Request sent. Continuing with other tasks...
// (network call completes...)
// Received: sunt aut facere repellat provident...Event Loop Order of Execution — Full Example
console.log("A"); // Synchronous
setTimeout(() => console.log("B"), 0); // Macrotask
Promise.resolve()
.then(() => console.log("C")) // Microtask 1
.then(() => console.log("D")); // Microtask 2 (chained)
queueMicrotask(() => console.log("E")); // Microtask 3
console.log("F"); // Synchronous
// Output: A, F, C, E, D, BExecution Breakdown:
- A — synchronous
- F — synchronous
- C — microtask 1 (Promise)
- E — microtask 3 (queueMicrotask)
- D — microtask 2 (chained .then runs after C resolves)
- B — macrotask (setTimeout)
Key Points to Remember
- JavaScript is single-threaded — only one thing runs at a time in the call stack
- The event loop moves tasks from queues to the call stack when the stack is empty
- Async operations (setTimeout, fetch) are handled by Web APIs — not by the JavaScript engine directly
- Microtasks (Promises) always run before macrotasks (setTimeout, setInterval)
- After every macrotask, all pending microtasks are processed before the next macrotask begins
awaitsuspends an async function and resumes it as a microtasksetTimeout(fn, 0)does not run immediately — it waits for the stack to clear and all microtasks to finish- Understanding the event loop is key to predicting execution order in async JavaScript
