JavaScript code within one agent runs one job at a time, yet browsers and Node.js coordinate many concurrent operations through their host event loops and platform services. Workers can add other agents and threads. When interviewers ask you to predict the output of setTimeout mixed with Promises, they are testing whether you understand tasks, microtask checkpoints, and host-specific scheduling.
Table of Contents
- Event Loop Fundamentals Questions
- Microtasks vs Macrotasks Questions
- Code Output Prediction Questions
- Async/Await Questions
- Node.js Event Loop Questions
- Practical Application Questions
- Quick Reference
Event Loop Fundamentals Questions
These questions test your understanding of the event loop's core mechanism.
What is the JavaScript Event Loop?
The browser event loop coordinates tasks, microtasks, rendering, and asynchronous platform work for an agent. It selects a runnable task, performs that task's steps, then runs a microtask checkpoint. A rendering opportunity may follow according to the browser's scheduling and display constraints.
During a JavaScript job, function execution contexts form a stack. Calling setTimeout registers a timer with the host; once the delay threshold has passed, the host queues a timer task. The callback runs only when the event loop selects that runnable task. A timer delay is a minimum threshold, not an exact execution time.
What are the main components of the Event Loop?
The Event Loop consists of several interconnected parts that work together to manage asynchronous execution:
| Component | Purpose |
|---|---|
| Call Stack | Where function execution contexts are managed (LIFO) |
| Web APIs | Browser-provided APIs that handle async operations (setTimeout, fetch, DOM events) |
| Task Queues | One or more queues associated with task sources such as timers, networking, and user interaction |
| Microtask Queue | Holds Promise callbacks, queueMicrotask, MutationObserver |
| Rendering Opportunities | Browser-controlled opportunities to run animation callbacks, update layout, and paint |
| Event Loop | Selects runnable tasks and performs microtask checkpoints |
A useful browser simplification is: run one task → drain the microtask queue → possibly render → select another task. The browser may choose among multiple task queues, so "one global macrotask FIFO" is not the standard model.
What happens to the event loop during a long-running script?
The call stack must be empty for the event loop to process any tasks. A long-running synchronous script blocks everything—no events fire, no callbacks execute, the UI freezes.
This is why we never put heavy computation in the main thread without breaking it up. If you have a loop processing 10,000 items synchronously, the browser cannot respond to user clicks, animations freeze, and the page becomes unresponsive.
Microtasks vs Macrotasks Questions
These questions test your understanding of task queue priorities.
What is the difference between microtasks and macrotasks?
After the current task finishes, the event loop performs a microtask checkpoint and keeps dequeuing microtasks until the queue is empty. Microtasks queued by other microtasks run in the same checkpoint, which is why an unbounded chain can delay tasks and rendering.
Microtasks:
- Promise callbacks (
.then(),.catch(),.finally()) queueMicrotask()- MutationObserver
Regular tasks (often called macrotasks informally):
setTimeout/setInterval- I/O operations
- Event handlers (click, scroll, etc.)
Rendering is scheduled through rendering opportunities rather than being just another macrotask. In the usual example where an already-settled Promise reaction and a zero-delay timer are scheduled by the same task, the Promise reaction runs first at the microtask checkpoint.
Why does Promise execute before setTimeout even with 0 delay?
Promise reactions use the microtask queue, while setTimeout queues a timer task after its delay threshold. When both are scheduled during the same task, the microtask checkpoint occurs before the event loop can select the timer task.
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// Output: 1, 4, 3, 2Step by step:
console.log('1')- Runs immediately, prints1setTimeout- Timer registered; its callback becomes a later timer taskPromise.then- Callback sent to microtask queueconsole.log('4')- Runs immediately, prints4- Current task finishes → microtask checkpoint prints
3 - A later event-loop turn selects the timer task → prints
2
What is the difference between setTimeout(fn, 0) and queueMicrotask(fn)?
setTimeout(fn, 0) schedules a timer task after a minimum delay, while queueMicrotask(fn) appends directly to the current event loop's microtask queue. If both calls occur in the same task as shown below, the microtask runs first.
Use queueMicrotask for a small follow-up that must run after the current synchronous work but before the event loop selects another task. Do not use it to yield for rendering: recursively queued microtasks can starve rendering and input.
setTimeout(() => console.log('macrotask'), 0);
queueMicrotask(() => console.log('microtask'));
console.log('sync');
// Output: sync, microtask, macrotaskCode Output Prediction Questions
These questions test your ability to trace through async code execution.
What is the output of this Promise and setTimeout combination?
This is a classic interview question that separates candidates who memorized answers from those who truly understand:
console.log('start');
setTimeout(() => console.log('timeout 1'), 0);
Promise.resolve()
.then(() => {
console.log('promise 1');
setTimeout(() => console.log('timeout 2'), 0);
})
.then(() => console.log('promise 2'));
setTimeout(() => console.log('timeout 3'), 0);
console.log('end');Output:
start
end
promise 1
promise 2
timeout 1
timeout 3
timeout 2
Explanation:
- Sync code runs:
start,end - Microtasks run:
promise 1, thenpromise 2(chained.then) - During
promise 1, a newsetTimeoutis queued (timeout 2) - These timer tasks run in registration order:
timeout 1,timeout 3,timeout 2
What happens with chained Promise.then callbacks?
In this example, each .then() queues the next Promise reaction while the same microtask checkpoint is being drained, so the whole chain runs before the already-scheduled timer task.
setTimeout(() => console.log('timeout'), 0);
Promise.resolve()
.then(() => console.log('promise 1'))
.then(() => console.log('promise 2'))
.then(() => console.log('promise 3'));
console.log('sync');Output:
sync
promise 1
promise 2
promise 3
timeout
Each .then() callback is added to the microtask queue when the previous promise resolves, and the entire microtask queue is drained before processing the setTimeout.
What is the output when a Promise executor runs synchronously?
The Promise constructor's executor function runs synchronously—only the .then() callbacks are asynchronous:
const promise = new Promise((resolve) => {
console.log('1');
resolve();
console.log('2');
});
promise.then(() => console.log('3'));
console.log('4');
// Output: 1, 2, 4, 3The executor runs immediately when the Promise is created, so 1 and 2 print synchronously. The resolve() call queues the .then() callback as a microtask, which runs after 4.
Async/Await Questions
These questions test your understanding of how async/await works with the event loop.
How does async/await relate to the event loop?
Async functions and await are specified in terms of Promise capabilities and jobs. An async function runs synchronously until it reaches an await; resuming it after the awaited value settles is scheduled through a Promise job, observed as a microtask in browsers and Node.js.
async function example() {
console.log('1');
await Promise.resolve();
console.log('2'); // This goes to microtask queue
}
example();
console.log('3');
// Output: 1, 3, 2The function runs synchronously until await, then pauses. The remaining code (console.log('2')) becomes a microtask.
What is the execution order of async functions?
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
async1();
console.log('script end');Output:
script start
async1 start
async2
script end
async1 end
Explanation:
script startprints synchronouslyasync1()is called, printsasync1 startasync2()is called, printsasync2synchronouslyawaitpauses async1, the rest becomes a microtaskscript endprints synchronously- Call stack empty → microtask runs →
async1 endprints
Node.js Event Loop Questions
These questions test your understanding of Node.js-specific event loop behavior.
How is the Node.js event loop different from the browser?
The Node.js event loop has multiple phases that process different types of callbacks. It uses the libuv library for async I/O operations.
Node.js Event Loop Phases:
- Timers - executes setTimeout and setInterval callbacks
- Pending callbacks - executes I/O callbacks deferred from previous cycle
- Idle, prepare - internal use only
- Poll - retrieves new I/O events
- Check - executes setImmediate callbacks
- Close callbacks - executes close event callbacks
Key differences include setImmediate() in the check phase, libuv-backed I/O, and a separate process.nextTick() queue. Starting with libuv 1.45 (Node.js 20), timers run after the poll phase during loop iterations, which can affect the ordering of timers and setImmediate().
What is process.nextTick in Node.js?
process.nextTick() schedules work in Node's separate next-tick queue and is now documented as a legacy API for most userland deferral. Recursively filling that queue can starve I/O. In CommonJS the next-tick queue is drained before the Promise/queueMicrotask queue, but ESM top-level evaluation already runs as a microtask, so the simple ordering can reverse.
// Node.js CommonJS example
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// Output: nextTick, promisePrefer queueMicrotask() for portable deferral unless you specifically need process.nextTick() semantics.
Practical Application Questions
These questions test your ability to apply event loop knowledge to real problems.
How do you break up heavy computation to avoid blocking?
Long-running synchronous code blocks the UI. Break it into smaller chunks and yield to the event loop between chunks:
// BAD: Blocks the UI
function processLargeArray(array) {
array.forEach(item => heavyComputation(item));
}
// GOOD: Yields to the event loop
function processLargeArrayAsync(array) {
let index = 0;
function processChunk() {
const chunkSize = 100;
const end = Math.min(index + chunkSize, array.length);
while (index < end) {
heavyComputation(array[index]);
index++;
}
if (index < array.length) {
setTimeout(processChunk, 0); // Yield to event loop
}
}
processChunk();
}Using setTimeout(fn, 0) between chunks allows the browser to handle user events and render updates.
How do you let the browser paint before blocking work?
Returning from the current task does not by itself guarantee that a paint has happened; rendering depends on browser-controlled rendering opportunities. requestAnimationFrame() runs before a paint, so calling alert() inside the first callback can still block that paint. A second animation-frame callback schedules the blocking work for a later frame, after the browser has had an opportunity to present the update:
// BAD: Alert shows before DOM updates
button.textContent = 'Loading...';
alert('Processing!'); // Blocks - user sees old text
// Allow a rendering opportunity before blocking in a later frame
button.textContent = 'Loading...';
requestAnimationFrame(() => {
requestAnimationFrame(() => {
alert('Processing!');
});
});How does debouncing work with the event loop?
Debouncing uses setTimeout and clearTimeout to ensure a function only runs after a period of inactivity:
let timeoutId;
searchInput.addEventListener('input', (e) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
// Only runs after user stops typing for 300ms
performSearch(e.target.value);
}, 300);
});Each keystroke clears the previous timer and sets a new one. The search only runs when no new keystrokes occur for 300ms.
Quick Reference
| Concept | What to Remember |
|---|---|
| Call Stack | LIFO, synchronous execution |
| Task queues | One or more queues; timers, I/O, and UI events come from different task sources |
| Microtask Queue | Promises, queueMicrotask, MutationObserver |
| Browser checkpoint | Task → drain microtasks → possible rendering opportunity → next task |
| Promise vs setTimeout | If scheduled in the same task, an already-settled Promise reaction runs before the timer |
| Blocking | Long sync code freezes everything |
| async/await | Code after await goes to microtask queue |
| process.nextTick | Node.js legacy queue; CommonJS and ESM can differ in ordering |
Related Articles
- Complete Frontend Developer Interview Guide - comprehensive preparation guide for frontend interviews
- Complete Node.js Backend Developer Interview Guide - comprehensive preparation guide for backend interviews
- JavaScript Closures Interview Guide - Understanding closures is essential for hooks and callbacks
- Node.js Advanced Interview Guide - Event loop, streams, and Node.js internals
- 17 Tricky JavaScript Interview Questions - The gotchas that catch most candidates off guard
- Top 5 JavaScript Interview Mistakes - Common pitfalls including event loop misconceptions
Official Sources
- HTML Standard: event loops - task queues, microtask checkpoints, and rendering opportunities
- HTML Standard: timers and microtask queuing - timer thresholds,
queueMicrotask(), and rendering guidance - Node.js: The Event Loop - libuv phases, Node 20 timer change, and
setImmediate() - Node.js
process.nextTick()documentation - legacy status, starvation risk, and CommonJS/ESM ordering
Frequently Asked Questions
What is the JavaScript Event Loop?
An event loop coordinates tasks, microtasks, rendering, and asynchronous platform operations for a JavaScript agent. It selects a runnable task, runs its steps, performs a microtask checkpoint, and gives the browser rendering opportunities. JavaScript in one agent executes one job at a time, while browsers can also use workers and parallel platform services.
What is the difference between microtasks and macrotasks?
A browser event loop has one microtask queue and one or more task queues. After a task finishes, the browser drains microtasks such as Promise reactions and queueMicrotask callbacks before selecting another task. 'Macrotask' is informal shorthand for a regular task; rendering is a separate opportunity, not simply another macrotask.
Why does Promise execute before setTimeout even with 0 delay?
When both are scheduled by the same task, a reaction to an already-resolved Promise is a microtask, while setTimeout schedules a later timer task after at least its delay threshold. The microtask checkpoint runs when the current task completes, so that Promise reaction runs before the timer in the standard interview example. This is a contextual rule, not a claim that every Promise callback always beats every timer.
What happens if the call stack is blocked?
If the call stack is blocked by long-running synchronous code, the event loop cannot process any other tasks. This freezes the UI, prevents user interactions, and stops any callbacks from executing. This is why heavy computations should be broken into smaller chunks, moved to Web Workers, or made asynchronous to keep the application responsive.
What are the main components of the Event Loop?
For browser interviews, discuss the JavaScript execution stack, platform APIs, one or more task queues grouped by task source, the microtask queue, and rendering opportunities. The event loop selects a runnable task, executes it, then performs a microtask checkpoint. The HTML Standard does not define one universal FIFO 'macrotask queue'.
How is the Node.js event loop different from the browser?
Node.js uses libuv phases including timers, pending callbacks, poll, check, and close callbacks; setImmediate runs in check. Since libuv 1.45 in Node 20, timers run after poll during loop iterations. process.nextTick has a separate legacy queue and can starve I/O, but its ordering relative to Promise microtasks differs between CommonJS and ESM. Prefer queueMicrotask for portable deferral.
