Predict the Output: 10 Event Loop Questions Every Front-End Developer Should Practise
Asynchronous execution and the event loop represent one of the most frequently tested topics in front-end technical interviews. Interviewers use "predict the output" questions during live coding rounds and technical phone screens to see if candidates truly understand how the JavaScript engine schedules tasks behind the scenes.
Rather than testing rote syntax, these questions evaluate your mental model of the call stack, the microtask queue, the macrotask (task) queue, and rendering boundaries. Strong candidates do not guess; they trace execution phase by phase, explaining the exact order of resolution out loud.
This article targets developers with 0 to 5 years of experience preparing for junior to mid-level front-end roles.
| # | Question | Level |
|---|---|---|
| 1 | What is the output of basic synchronous code mixed with setTimeout(0)? | Junior |
| 2 | What is the output when a Promise microtask competes with a setTimeout timer? | Junior |
| 3 | What is the output when code runs inside the new Promise() constructor? | Junior |
| 4 | What is the output when multiple Promise .then chains interleave? | Junior |
| 5 | What is the output of an async/await function call compared to top-level code? | Mid |
| 6 | What is the output when mixing async/await, setTimeout, and Promise resolution? | Mid |
| 7 | What is the output when a microtask is scheduled inside a macrotask callback? | Mid |
| 8 | What is the output when interleaving queueMicrotask and Promise .then? | Mid |
| 9 | What is the output when a Promise .then handler returns another resolved Promise? | Senior |
| 10 | What is the output when an async function returns a rejected Promise without await? | Senior |
Q1. What is the output of basic synchronous code mixed with setTimeout(0)?
Level: Junior
What the interviewer is checking: The interviewer wants to verify that you understand JavaScript's single-threaded nature and that asynchronous callbacks cannot execute until the call stack is completely clear.
Snippet:
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
console.log('3');
Your answer: Try to predict the output before reading on.
Output:
1
3
2
Short answer (say this first):
Synchronous statements execute immediately on the call stack, so 1 and 3 log first. Passing 0 to setTimeout does not run the callback immediately; instead, it registers a timer with the browser or runtime environment, which enqueues the callback into the macrotask queue. The event loop only pushes this callback onto the call stack after all synchronous execution completes.
Detailed explanation:
console.log('1')enters the call stack, logs1, and pops off.setTimeout(..., 0)executes synchronously. The host environment sets a timer and queues its callback into the macrotask (task) queue.console.log('3')enters the call stack, logs3, and pops off.- The call stack is now empty. The event loop inspects the queues, retrieves the timer callback from the macrotask queue, pushes it to the call stack, and logs
2.
Common mistakes:
- Believing that a delay of
0means zero delay or immediate execution before the next line of code.
Likely follow-up questions:
- Why can
setTimeout(..., 0)take more than 0ms in browsers? — The HTML living standard enforces a minimum 4ms clamping delay for nested timers deeper than 5 levels.
Q2. What is the output when a Promise microtask competes with a setTimeout timer?
Level: Junior
What the interviewer is checking: The interviewer is confirming whether you know the priority hierarchy between microtasks (Promises) and macrotasks (setTimeout).
Snippet:
console.log('Start');
setTimeout(() => {
console.log('Timeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('End');
Your answer: Try to predict the output before reading on.
Output:
Start
End
Promise
Timeout
Short answer (say this first):
Synchronous code runs first, logging Start and End. Then, the event loop drains the entire microtask queue before picking the next task from the macrotask queue. Because Promise .then handlers are queued as microtasks, Promise logs before the Timeout macrotask.
Detailed explanation:
- Synchronous code logs
Start. setTimeoutschedules its callback in the macrotask queue.Promise.resolve().then(...)queues its fulfillment reaction into the microtask queue.- Synchronous code logs
End. The initial script execution (macrotask) completes. - The event loop performs a microtask checkpoint: it processes the microtask queue and logs
Promise. - Once the microtask queue is empty, the event loop picks the next macrotask, logging
Timeout.
Common mistakes:
- Assuming tasks and microtasks share a single first-in, first-out (FIFO) queue based solely on registration timestamp.
Likely follow-up questions:
- What constitutes a microtask versus a macrotask in the browser? — Microtasks include Promise reactions,
queueMicrotask, andMutationObserver. Macrotasks includesetTimeout,setInterval,setImmediate(Node.js), and UI events.
Q3. What is the output when code runs inside the new Promise() constructor?
Level: Junior
What the interviewer is checking: The interviewer wants to verify that you know the executor function passed to new Promise(executor) runs synchronously, not asynchronously.
Snippet:
console.log('1');
const p = new Promise((resolve) => {
console.log('2');
resolve('3');
console.log('4');
});
p.then((val) => {
console.log(val);
});
console.log('5');
Your answer: Try to predict the output before reading on.
Output:
1
2
4
5
3
Short answer (say this first):
The executor function passed to new Promise executes synchronously at the point of instantiation. Therefore, 1, 2, and 4 log synchronously, followed by 5. Only the .then() handler is queued as a microtask, so 3 logs after the current synchronous tick finishes.
Detailed explanation:
console.log('1')executes synchronously, logging1.new Promise(executor)is evaluated. The executor runs synchronously immediately:console.log('2')logs2.resolve('3')transitions the promise state from pending to fulfilled with value'3'.console.log('4')logs4.
p.then(...)attaches a fulfillment handler. Becausepis already fulfilled, its handler is scheduled into the microtask queue.console.log('5')executes synchronously, logging5.- Call stack empties. The microtask queue runs, and the
.thencallback logs3.
Common mistakes:
- Believing everything inside
new Promise(...)runs asynchronously on a background thread.
Likely follow-up questions:
- Does calling
resolve()stop the remaining execution of the executor function? — No,resolve()simply mutates promise state; statements followingresolve()inside the executor continue running synchronously.
Q4. What is the output when multiple Promise .then chains interleave?
Level: Junior
What the interviewer is checking: The interviewer is testing your understanding of how chained .then callbacks schedule subsequent microtasks in turn.
Snippet:
Promise.resolve()
.then(() => {
console.log('Promise 1');
})
.then(() => {
console.log('Promise 2');
});
Promise.resolve()
.then(() => {
console.log('Promise 3');
})
.then(() => {
console.log('Promise 4');
});
Your answer: Try to predict the output before reading on.
Output:
Promise 1
Promise 3
Promise 2
Promise 4
Short answer (say this first):
Both initial .then() callbacks are queued in the microtask queue during synchronous execution. When the first microtask executes and logs Promise 1, returning undefined queues its chained .then (Promise 2) at the end of the microtask queue. Next, Promise 3 executes, which in turn queues Promise 4. This results in interleaved execution: Promise 1, Promise 3, Promise 2, Promise 4.
Detailed explanation:
- Script evaluates.
Promise 1callback is queued into the microtask queue:[P1]. - Second promise evaluates.
Promise 3callback is queued into the microtask queue:[P1, P3]. - Call stack empties. Microtask queue begins execution:
- Dequeue
P1: logsPromise 1. Its resolution queuesP2. Queue:[P3, P2]. - Dequeue
P3: logsPromise 3. Its resolution queuesP4. Queue:[P2, P4]. - Dequeue
P2: logsPromise 2. Queue:[P4]. - Dequeue
P4: logsPromise 4. Queue is now empty.
- Dequeue
Common mistakes:
- Assuming the entire first promise chain resolves completely before the second promise chain starts.
Likely follow-up questions:
- What happens if a
.thencallback returns a rejected promise or throws an error? — It rejects the chained promise and schedules any attached.catch()or rejection handler into the microtask queue.
Q5. What is the output of an async/await function call compared to top-level code?
Level: Mid
What the interviewer is checking: The interviewer wants to verify that you understand how async/await desugars into native Promises and microtasks.
Snippet:
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');
Your answer: Try to predict the output before reading on.
Output:
script start
async1 start
async2
script end
async1 end
Short answer (say this first):
Code inside an async function executes synchronously until the first await expression. When await async2() is evaluated, async2() runs synchronously, and the remainder of async1 is suspended and scheduled as a microtask. Synchronous top-level code finishes logging script end before the suspended async1 end resumes.
Detailed explanation:
console.log('script start')runs synchronously: logsscript start.async1()is invoked synchronously:console.log('async1 start')logsasync1 start.await async2()evaluatesasync2(), which logsasync2and returns a resolved promise.- The
awaitpauses execution ofasync1, queuing the continuation (console.log('async1 end')) into the microtask queue.
- Execution returns to the top-level script:
console.log('script end')logsscript end. - Call stack empties. The microtask queue runs, resuming
async1and loggingasync1 end.
Common mistakes:
- Thinking that declaring a function with
asyncmakes its entire body run asynchronously from the very start.
Likely follow-up questions:
- Does
awaitwrap non-promise values? — Yes,await ximplicitly evaluatesPromise.resolve(x), pausing execution and scheduling a microtask even ifxis a primitive value.
Q6. What is the output when mixing async/await, setTimeout, and Promise resolution?
Level: Mid
What the interviewer is checking: The interviewer is testing your ability to coordinate multiple asynchronous queues, including multiple await points and timers.
Snippet:
async function task() {
console.log('task start');
await Promise.resolve();
console.log('task middle');
await new Promise((r) => setTimeout(r, 0));
console.log('task end');
}
console.log('main start');
task();
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise then'));
console.log('main end');
Your answer: Try to predict the output before reading on.
Output:
main start
task start
main end
task middle
promise then
timeout
task end
Short answer (say this first):
main start and task start run synchronously, followed by main end. Then, microtasks run in order: the first await continuation logs task middle, and the standalone .then logs promise then. Finally, macrotasks execute: the standalone timeout logs, followed by the timer inside task, which resumes to log task end.
Detailed explanation:
- Logs
main start. - Calls
task(): logstask start. Hitsawait Promise.resolve(), queuing task continuation as Microtask 1. - Schedules
timeoutcallback as Macrotask 1. - Schedules
promise thencallback as Microtask 2. - Logs
main end. - Drains Microtasks:
- Microtask 1 runs: logs
task middle. Hitsawait new Promise(setTimeout), scheduling its timer callback as Macrotask 2. - Microtask 2 runs: logs
promise then.
- Microtask 1 runs: logs
- Processes Macrotasks:
- Macrotask 1 runs: logs
timeout. - Macrotask 2 runs: resolves inner promise, queuing task resumption as a microtask.
- Microtask runs: logs
task end.
- Macrotask 1 runs: logs
Common mistakes:
- Placing
task endahead oftimeout, failing to recognize thatawait new Promise(r => setTimeout(r, 0))defers execution to a subsequent macrotask turn.
Likely follow-up questions:
- How would replacing
setTimeout(r, 0)withscheduler.yield()change execution order in modern browsers? —scheduler.yield()yields control back to the event loop while retaining task continuation priority ahead of generic background macrotasks.
Q7. What is the output when a microtask is scheduled inside a macrotask callback?
Level: Mid
What the interviewer is checking: The interviewer wants to verify that you know the microtask queue is drained immediately after each macrotask completes, before the next macrotask starts.
Snippet:
setTimeout(() => {
console.log('timeout 1');
Promise.resolve().then(() => {
console.log('nested promise');
});
}, 0);
setTimeout(() => {
console.log('timeout 2');
}, 0);
Your answer: Try to predict the output before reading on.
Output:
timeout 1
nested promise
timeout 2
Short answer (say this first):
The event loop processes exactly one macrotask, then drains all available microtasks before moving to the next macrotask. When timeout 1 finishes, its nested promise microtask executes immediately, logging nested promise before the engine picks up timeout 2.
Detailed explanation:
- Both
setTimeouttimers register their callbacks into the macrotask queue:[timeout 1, timeout 2]. - Initial script finishes. The event loop picks the first macrotask:
timeout 1. timeout 1logstimeout 1and queues a microtask (nested promise).timeout 1completes. The event loop checks the microtask queue and findsnested promise.- The microtask queue drains: logs
nested promise. - With the microtask queue empty, the event loop picks the next macrotask from the queue: logs
timeout 2.
Common mistakes:
- Believing all pending macrotasks in the queue run before any newly scheduled microtasks can execute.
Likely follow-up questions:
- Can a recursive microtask starve the event loop? — Yes, recursively scheduling microtasks prevents the event loop from ever reaching subsequent macrotasks or UI rendering opportunities, freezing the page.
Q8. What is the output when interleaving queueMicrotask and Promise .then?
Level: Mid
What the interviewer is checking: The interviewer is confirming your understanding of explicit microtask APIs and how queueMicrotask shares the same underlying queue as Promises.
Snippet:
console.log('1');
setTimeout(() => console.log('2'), 0);
queueMicrotask(() => {
console.log('3');
queueMicrotask(() => console.log('4'));
});
Promise.resolve().then(() => console.log('5'));
console.log('6');
Your answer: Try to predict the output before reading on.
Output:
1
6
3
5
4
2
Short answer (say this first):
1 and 6 log synchronously. Then the microtask queue drains all items, including nested microtasks, before processing macrotasks. queueMicrotask (3) and Promise.resolve().then (5) were queued first; executing 3 queues 4, which runs before the setTimeout callback (2), resulting in 1, 6, 3, 5, 4, 2.
Detailed explanation:
- Synchronous code logs
1. setTimeoutenqueues2into the macrotask queue.queueMicrotaskenqueues3into the microtask queue:[3].Promise.resolve().thenenqueues5into the microtask queue:[3, 5].- Synchronous code logs
6. - Event loop enters microtask checkpoint:
- Dequeues
3: logs3, enqueues4. Microtask queue is now[5, 4]. - Dequeues
5: logs5. Microtask queue is[4]. - Dequeues
4: logs4. Microtask queue is now empty.
- Dequeues
- Event loop executes next macrotask: logs
2.
Common mistakes:
- Thinking
queueMicrotaskhas a separate queue with different priority than Promises.
Likely follow-up questions:
- When should you use
queueMicrotaskinstead ofPromise.resolve().then? — UsequeueMicrotaskwhen you need to schedule asynchronous cleanup or telemetry without the overhead of creating and discarding an unused Promise object.
Q9. What is the output when a Promise .then handler returns another resolved Promise?
Level: Senior
What the interviewer is checking: This is an advanced question testing your deep knowledge of the ECMAScript Job Queue specification regarding Promise unwrapping (PromiseResolveThenableJob).
Snippet:
Promise.resolve()
.then(() => {
console.log('A');
return Promise.resolve('B');
})
.then((val) => {
console.log(val);
});
Promise.resolve()
.then(() => {
console.log('C');
})
.then(() => {
console.log('D');
})
.then(() => {
console.log('E');
});
Your answer: Try to predict the output before reading on.
Output:
A
C
D
E
B
Short answer (say this first):
When a .then() callback returns a Promise instance, the ECMAScript specification requires the engine to unwrap it by enqueueing a PromiseResolveThenableJob. This unwrapping mechanism introduces two extra microtask ticks of delay before the outer chained .then receives the value. As a result, the parallel chain executes C, D, and E before B finally logs.
Detailed explanation:
Tick 1:Initial microtasks run.AandClog. Chain 1 returnsPromise.resolve('B').Tick 2:Instead of resolving immediately, the engine enqueues an internal job to unwrap the returned thenable. Meanwhile, Chain 2's second handler runs and logsD.Tick 3:The unwrapping job completes and schedules Chain 1's second handler (val => console.log(val)). Simultaneously, Chain 2's third handler runs and logsE.Tick 4:Chain 1's second handler finally executes and logsB.
Common mistakes:
- Assuming that returning a resolved Promise unwraps immediately on the next microtask tick.
Likely follow-up questions:
- What happens if you return a primitive value
'B'instead ofPromise.resolve('B')? — Returning a primitive resolves the chained promise in a single microtask tick, producingA, C, B, D, E.
Q10. What is the output when an async function returns a rejected Promise without await?
Level: Senior
What the interviewer is checking: The interviewer wants to verify that you understand error propagation, the difference between return p and return await p, and unhandled rejections.
Snippet:
async function faulty() {
try {
const p = Promise.reject('Rejected Error');
return p;
} catch (err) {
console.log('Caught inside:', err);
}
}
faulty()
.then(() => console.log('Resolved'))
.catch((err) => console.log('Caught outside:', err));
Your answer: Try to predict the output before reading on.
Output:
Caught outside: Rejected Error
Short answer (say this first):
return p returns the rejected promise directly without unwrapping or awaiting its resolution inside the try block. Because no error was thrown synchronously inside faulty(), the local catch block is bypassed. The outer caller receives the rejected promise, triggering its .catch() handler to log Caught outside: Rejected Error.
Detailed explanation:
faulty()is called. The engine creates a rejected Promisep.return preturns the promise object directly. The function completes successfully without throwing an exception.- Because no exception was thrown, the internal
try...catchblock never triggers. - The caller receives the returned promise, which is rejected with
'Rejected Error'. - The chained
.thenis skipped, and the.catchhandler runs as a microtask, loggingCaught outside: Rejected Error. - To catch the error locally, you must write
return await p;, which unwraps the rejection and throws it into the localcatchblock.
Common mistakes:
- Thinking that returning a rejected promise inside a
tryblock automatically triggers the correspondingcatchblock.
Likely follow-up questions:
- Why does ESLint have a
no-return-awaitrule, and why was it deprecated? — It was originally intended to avoid redundant microtask ticks, but was deprecated becausereturn awaitis necessary insidetry...catchand improves async stack traces in V8.
Preparation Checklist
- Practise tracing asynchronous snippets on paper by maintaining two columns: Microtasks and Macrotasks.
- Memorize the exact priority order: Synchronous call stack -> Microtasks (until empty) -> One Macrotask -> Render opportunity -> Repeat.
- Explain the difference between
return pandreturn await pout loud to ensure error handling fluency. - Understand why returning a Promise inside
.thendelays chained execution by two microtask ticks. - Review how
queueMicrotaskdiffers fromPromise.resolve().thenandsetTimeout(0). - Open Chrome DevTools Performance tab and inspect how long tasks block UI rendering opportunities.
Conclusion
Mastering the JavaScript event loop requires looking past syntax and visualizing the underlying queues. By methodically categorizing operations into synchronous stack frames, microtasks, and macrotasks, you can confidently predict code output, diagnose race conditions, and explain execution timing during front-end interviews.