What is the event loop in JavaScript?
Advertisement
🧩 Scenario
Architecture Walkthrough
The Call Stack and Single-Threaded Execution
JavaScript is single-threaded: it can only execute one thing at a time. The call stack tracks the current execution context. When a function is called, a new frame is pushed onto the stack. When the function returns, that frame is popped off. All synchronous code runs in this stack, and no other code can execute while a frame is on the stack.
If a synchronous operation takes a long time (a large loop, a heavy computation), the call stack remains occupied for that entire duration. During this time, no event handlers, no timer callbacks, and no Promise continuations can run. This is what "blocking the main thread" means, and it is the root cause of UI freezes and unresponsive pages.
Web APIs, Task Queues, and the Event Loop
When JavaScript calls a Web API like setTimeout, fetch, or addEventListener, the work is handed off to the browser's (or Node.js's) runtime environment, which handles it outside the JavaScript thread. When the runtime work completes (timer fires, network responds, user clicks), the callback is placed in a queue.
The event loop is a continuous process that checks one condition: is the call stack empty? If yes, it takes the next task from a queue and pushes it onto the stack for execution. This is how JavaScript achieves non-blocking behavior without threads: by deferring completion callbacks to run between synchronous execution blocks.
Microtasks vs Macrotasks
There are two kinds of queues. The microtask queue holds Promise callbacks (.then, .catch, .finally) and queueMicrotask calls. The macrotask queue (also called the task queue) holds setTimeout, setInterval, setImmediate (Node.js), and I/O callbacks.
The critical ordering rule is: after each macrotask completes, the event loop drains the entire microtask queue before picking up the next macrotask. This means that no matter how many .then callbacks are chained, they all run to completion before the next setTimeout callback starts. This is why Promise.resolve().then(...) always logs before setTimeout(..., 0) even when both are scheduled at the same moment.
Key Code Explained
console.log('1: synchronous');
setTimeout(() => console.log('2: macrotask (setTimeout)'), 0);
Promise.resolve()
.then(() => console.log('3: microtask (Promise)'))
.then(() => console.log('4: chained microtask'));
queueMicrotask(() => console.log('5: microtask (queueMicrotask)'));
console.log('6: synchronous');
// Output order:
// 1: synchronous
// 6: synchronous
// 3: microtask (Promise)
// 5: microtask (queueMicrotask)
// 4: chained microtask
// 2: macrotask (setTimeout)
Walking through the execution: lines 1 and 6 run synchronously on the call stack. The setTimeout callback is registered with the browser runtime and placed in the macrotask queue. The Promise.resolve().then() callbacks and the queueMicrotask callback are placed in the microtask queue. After the call stack empties, the microtask queue drains completely (items 3, 5, and the chained item 4). Only then does the event loop pick up the macrotask at item 2.
Tradeoffs
| Queue type | Examples | When it runs |
|---|---|---|
| Synchronous (call stack) | Regular function calls, loops | Immediately, blocking all other execution |
| Microtask queue | Promise.then, queueMicrotask | After current task, before next macrotask, full drain |
| Macrotask queue | setTimeout, setInterval, I/O | One per event loop tick, after microtask queue empties |
What Interviewers Actually Check
- Whether you can correctly predict the output order of mixed sync, Promise, and setTimeout code
- Whether you know that microtasks drain completely before the next macrotask runs
- Whether you understand why
setTimeout(fn, 0)does not mean "run immediately" - Whether you can explain the consequence of blocking the call stack with synchronous code
- Whether you can name real examples of microtasks and macrotasks
Follow-Up Questions
- What is
queueMicrotaskand how does it differ from wrapping code inPromise.resolve().then()? - How does the Node.js event loop differ from the browser event loop? What is the
nextTickqueue? - If you nest a
setTimeoutinside aPromise.then, what order does the callback run relative to other pending microtasks? - What happens to a
setIntervalcallback when the call stack is blocked for longer than the interval duration? - A frontend engineer reports that their React UI feels janky when processing a large array. How would you explain the event loop's role in the problem and propose a fix?
Common Candidate Mistakes
- Thinking
setTimeout(fn, 0)executes the callback immediately when it always goes to the macrotask queue - Not knowing that all microtasks drain before the next macrotask, which causes incorrect output order predictions
- Confusing the call stack (where code executes) with the task queue (where pending callbacks wait)
- Assuming Node.js and browser event loops are identical when Node.js has additional phases like
process.nextTick - Saying JavaScript is "asynchronous" without clarifying that it is single-threaded and only appears concurrent through event loop scheduling
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you describe the call stack, microtask queue, and macrotask queue and how each feeds into the event loop?
- Can you predict the output order of code that mixes synchronous statements,
setTimeout, andPromise.resolve()? - Can you explain why Promise callbacks run before
setTimeoutcallbacks even whensetTimeouthas zero delay? - Can you describe what happens when the call stack is blocked with a long synchronous operation?
- Can you name at least two examples of microtasks and two examples of macrotasks?
Summary
The JavaScript event loop is the mechanism that enables non-blocking asynchronous behavior in a single-threaded runtime. Synchronous code runs on the call stack. Async work is offloaded to the runtime environment (browser APIs or Node.js), and completion callbacks are placed in queues: the microtask queue for Promise callbacks and the macrotask queue for timers and I/O.
The event loop's rule is simple: when the call stack is empty, drain all microtasks, then pick one macrotask. This ordering means that Promise continuations always run before the next timer callback, regardless of when each was scheduled.
Understanding this model is necessary for debugging async ordering bugs, avoiding UI jank caused by long synchronous operations, and reasoning about why setTimeout(fn, 0) does not mean "run now." It is one of the most reliable signals of deep JavaScript knowledge in an interview.
Does JavaScript run multiple threads?
No. JavaScript is single-threaded but uses asynchronous callbacks managed by the event loop to avoid blocking.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement