The JavaScript Event Loop Explained
JavaScript is single-threaded. It can only execute one piece of code at a time. Yet it handles thousands of asynchronous operations-network requests, timers, user events-without blocking. The mechanism that makes this possible is the Event Loop.
Core Components
The Event Loop relies on several components working together:
- Call Stack – Executes synchronous code
- Web APIs – Handles async operations (timers, fetch, DOM events)
- Callback Queue (Task Queue) – Holds callbacks from Web APIs
- Microtask Queue – Holds promise callbacks and queueMicrotask
- Event Loop – Moves tasks from queues to the call stack
The Call Stack
The call stack is where JavaScript executes functions. When you call a function, it's pushed onto the stack. When it returns, it's popped off.
function first() {
console.log('first');
}
function second() {
first();
console.log('second');
}
second();Execution order:
second()is pushed onto the stackfirst()is called, pushed onto the stackconsole.log('first')executes,first()pops offconsole.log('second')executes,second()pops off- Stack is empty
Web APIs
When you call an async function like setTimeout, fetch, or add an event listener, JavaScript hands it off to the browser's Web APIs. These run outside the JavaScript engine.
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
console.log('End');
// Output:
// Start
// End
// TimeoutEven with a 0ms delay, setTimeout goes through the Web API and callback queue, so it runs after the synchronous code.
The Callback Queue (Task Queue)
When a Web API completes (timer fires, fetch returns, user clicks), the callback is placed in the Callback Queue (also called Task Queue or Macrotask Queue).
The Event Loop checks: "Is the call stack empty?" If yes, it takes the first callback from the queue and pushes it onto the stack for execution.
setTimeout(() => console.log('Task 1'), 0);
setTimeout(() => console.log('Task 2'), 0);
// Output:
// Task 1
// Task 2 (in order)The Microtask Queue
Microtasks have higher priority than regular tasks. They include:
- Promise
.then(),.catch(),.finally()callbacks queueMicrotask()MutationObservercallbacks
Rule: All microtasks are executed before the next task from the callback queue.
console.log('Start');
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
console.log('End');
// Output:
// Start
// End
// Promise
// TimeoutThe promise callback runs before the setTimeout callback because microtasks are processed first.
Event Loop Algorithm
Here's how the Event Loop works, step by step:
- Execute all synchronous code in the call stack
- When the stack is empty, process all microtasks
- Render the page (if needed) – style calculations, layout, paint
- Take one task from the callback queue
- Execute that task
- Repeat from step 2
Why Only ONE Task Per Loop?
You might wonder: why does the Event Loop process ALL microtasks but only ONE macrotask per iteration?
The answer: rendering and responsiveness.
If the Event Loop processed all macrotasks before rendering, a burst of setTimeout callbacks could block the page for seconds:
// If all tasks ran before render, this would freeze the page
for (let i = 0; i < 1000; i++) {
setTimeout(() => heavyComputation(), 0);
}By executing only one task per iteration, the browser gets a chance to:
- Check if a render is needed
- Process user input (clicks, typing)
- Keep the page responsive
Microtasks are different. They're meant for short, promise-related work, and they must complete before the current "logical operation" finishes. If you create an infinite loop of microtasks, you will block the page.
The render step is important: the browser checks if a repaint is needed (typically at 60fps, every ~16ms). This is why requestAnimationFrame callbacks run before the next paint.
Common Patterns and Examples
Mixing Promises and setTimeout
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve()
.then(() => console.log('3'))
.then(() => console.log('4'));
console.log('5');
// Output:
// 1
// 5
// 3
// 4
// 2Explanation:
console.log('1')– sync, runs immediatelysetTimeout– scheduled as task- Promise
.then()– scheduled as microtask console.log('5')– sync, runs immediately- Stack empty → process microtasks:
3, then4 - Microtasks done → process task:
2
Nested Microtasks
Promise.resolve().then(() => {
console.log('Promise 1');
Promise.resolve().then(() => {
console.log('Promise 2');
});
});
setTimeout(() => console.log('Timeout'), 0);
// Output:
// Promise 1
// Promise 2
// TimeoutThe nested promise is a new microtask, so it runs before the setTimeout.
queueMicrotask()
console.log('Start');
queueMicrotask(() => console.log('Microtask'));
setTimeout(() => console.log('Timeout'), 0);
console.log('End');
// Output:
// Start
// End
// Microtask
// TimeoutqueueMicrotask() adds a microtask directly, without creating a promise.
async/await and the Event Loop
async/await is syntactic sugar over promises. When you await, the function pauses and the code after await becomes a microtask.
async function example() {
console.log('1');
await Promise.resolve();
console.log('2'); // This becomes a microtask
}
console.log('A');
example();
console.log('B');
// Output:
// A
// 1
// B
// 2Explanation:
console.log('A')– syncexample()called,console.log('1')– syncawaitpausesexample(), rest becomes microtaskconsole.log('B')– sync- Stack empty → execute microtask:
console.log('2')
Blocking the Event Loop
Since JavaScript is single-threaded, long-running synchronous code blocks everything:
// This blocks the entire page for 5 seconds
const start = Date.now();
while (Date.now() - start < 5000) {
// Blocking loop
}
console.log('Done');During this time:
- No user events are processed
- No timers fire
- The page is unresponsive
Solution: Break work into smaller chunks or use Web Workers.
// Non-blocking approach
function processChunk(items, index = 0) {
const chunkSize = 100;
const end = Math.min(index + chunkSize, items.length);
for (let i = index; i < end; i++) {
// Process item
}
if (end < items.length) {
setTimeout(() => processChunk(items, end), 0);
}
}Node.js Event Loop Differences
Node.js has a similar but more complex event loop with additional phases:
- Timers – setTimeout, setInterval
- Pending callbacks – I/O callbacks
- Idle, prepare – internal use
- Poll – retrieve new I/O events
- Check – setImmediate callbacks
- Close callbacks – socket.on('close')
Node.js also has process.nextTick(), which runs before any other microtasks:
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
// Output:
// nextTick
// PromiseInterview Question: What is the order of execution for setTimeout, Promise, and process.nextTick in Node.js?
Answer:
process.nextTickruns first (before microtasks), then Promise callbacks (microtasks), then setTimeout (macrotask). The order is: nextTick → Promise → setTimeout.
Common Interview Questions
Interview Question: What is the difference between the Callback Queue and the Microtask Queue?
Answer: The Microtask Queue has higher priority. After the call stack is empty, ALL microtasks are processed before the Event Loop takes even ONE task from the Callback Queue. Microtasks include Promise callbacks and queueMicrotask. The Callback Queue holds setTimeout, setInterval, and I/O callbacks.
Interview Question: Why does a setTimeout with 0ms delay not execute immediately?
Answer: setTimeout(fn, 0) schedules the callback as a task in the Callback Queue. Even with 0ms, the callback must wait for: (1) the call stack to be empty, (2) all synchronous code to finish, and (3) all microtasks to be processed. Only then does the Event Loop pick up the setTimeout callback.
Interview Question: Can you block the Event Loop? What are the consequences?
Answer: Yes, any long-running synchronous code blocks the Event Loop. Consequences: the page becomes unresponsive, user events aren't processed, animations freeze, and timers are delayed. To avoid this, break heavy computation into chunks using setTimeout or requestAnimationFrame, or move work to a Web Worker.
Summary
| Component | Description | Priority |
|---|---|---|
| Call Stack | Executes sync code | Immediate |
| Microtask Queue | Promise callbacks, queueMicrotask | High |
| Callback Queue | setTimeout, I/O, events | Normal |
Key rules:
- Synchronous code runs first
- When the stack is empty, ALL microtasks run
- Then ONE task from the callback queue runs
- Repeat
Understanding the Event Loop is essential for writing performant, non-blocking JavaScript code.
Loading comments...