Explain the Event Loop and Execution Order in JavaScript
Advertisement
🧩 Scenario
Architecture Walkthrough
The Mental Model: Call Stack, Queues, and the Loop
JavaScript executes in a single thread. The call stack holds the currently executing functions. When the stack is empty, the event loop checks the queues. The microtask queue is checked first and fully drained: every pending microtask runs, and if any microtask enqueues another microtask, that new one also runs before the loop moves on. Only when the microtask queue is empty does the event loop pick up the next item from the macrotask queue.
This ordering rule is the foundation of all async execution order questions. Microtasks (Promise callbacks, queueMicrotask) always run before macrotasks (setTimeout, setInterval, I/O). Synchronous code runs before both because it runs directly on the call stack, not through a queue at all.
async/await Through the Lens of the Event Loop
async/await is syntactic sugar over Promises. An async function runs synchronously until it hits an await. At that point, the current async function suspends and returns a Promise to its caller. The code after the await is scheduled as a microtask to resume when the awaited Promise settles. The call stack is now free to run other synchronous code.
This means that while an async function is waiting at an await, other synchronous code, then other microtasks, then macrotasks can all run. The code after await is not guaranteed to run "next" unless no other microtasks are queued.
Predicting Execution Order
A reliable step-by-step approach for tracing execution: first identify all synchronous code (runs immediately, in order). Then identify all microtasks (runs in the order they were enqueued, after all synchronous code finishes). Then identify all macrotasks (runs one at a time, after the microtask queue drains between each). When microtasks enqueue more microtasks, add them to the current drain cycle before any macrotask runs.
Key Code Explained
// Classic output-order question
console.log('1 — sync start');
setTimeout(() => console.log('5 — macrotask'), 0);
Promise.resolve()
.then(() => {
console.log('3 — microtask');
return Promise.resolve(); // adds one more .then to microtask queue
})
.then(() => console.log('4 — microtask chained'));
console.log('2 — sync end');
// Output: 1, 2, 3, 4, 5
// 1 and 2: synchronous, run first
// 3 and 4: microtasks drain before macrotask
// 5: macrotask runs last
// async/await version of the same ordering
async function run() {
console.log('B — sync inside async fn');
await Promise.resolve(); // suspends; schedules resumption as microtask
console.log('D — microtask resume'); // runs after other sync, before setTimeout
}
console.log('A — sync before');
run();
console.log('C — sync after');
setTimeout(() => console.log('E — macrotask'), 0);
// Output: A, B, C, D, E
// Harder: nested async functions and interleaved Promises
async function inner() {
console.log('inner sync');
await null;
console.log('inner after await'); // microtask
}
async function outer() {
console.log('outer sync');
await inner(); // suspends outer until inner's promise settles
console.log('outer after inner'); // microtask — runs after inner completes
}
console.log('before');
outer();
console.log('after');
// Output: before, outer sync, inner sync, after, inner after await, outer after inner
The async/await interleaving example shows the most important insight: outer starts synchronously, calls inner synchronously up to inner's await, then returns to the caller (outer) which continues synchronously to its own await. Only after all synchronous code finishes do the microtasks (the code after each await) resume, from inner to outer.
Tradeoffs
| Code type | Queue | When it runs |
|---|---|---|
| Synchronous code | Call stack | Immediately, blocking all other execution |
| Promise .then / await | Microtask queue | After current task, fully drained before next macrotask |
| setTimeout / setInterval | Macrotask queue | One per loop tick, only after microtask queue is empty |
What Interviewers Actually Check
- Whether you can trace execution through a mixed code snippet and give the exact output order
- Whether you know that all microtasks drain before the next macrotask, not just the first pending one
- Whether you can explain what
awaitdoes to the current async function without saying "it pauses JavaScript" - Whether you know that code after
awaitresumes as a microtask, not synchronously - Whether you can describe the consequence of a large synchronous operation inside an async function
Follow-Up Questions
- You have five
await nullcalls in a row inside an async function. How many times does the event loop yield before the last line runs? - What is the difference between
Promise.resolve().then(fn)andqueueMicrotask(fn)? - If an async function throws synchronously before its first
await, when does the catch handler run? - How does
process.nextTickin Node.js fit into this model relative to Promise microtasks? - A UI framework batches state updates to avoid excessive re-renders. How does it use microtasks to implement this batching?
Common Candidate Mistakes
- Saying
setTimeout(fn, 0)runs immediately after the current synchronous code because the delay is 0 - Not knowing that all queued microtasks drain completely (including newly enqueued ones) before any macrotask
- Saying
awaitpauses the thread or JavaScript engine when it only suspends the current async function - Not knowing that code after
awaitresumes as a microtask and can be interleaved with other microtasks - Predicting that a chained
.theninside another.thenruns after the nextsetTimeoutwhen it is still a microtask
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you trace through a mixed sync/Promise/setTimeout snippet and predict the exact output order?
- Can you explain why
Promise.resolve().then()always runs beforesetTimeout(fn, 0)? - Can you explain what
awaitdoes to the current async function and what runs while it waits? - Can you describe the consequence of running a large synchronous loop inside an async function?
- Can you name two microtask sources and two macrotask sources from memory?
Summary
JavaScript's event loop processes tasks in a strict order: synchronous code runs first on the call stack, then the microtask queue drains completely (including any microtasks enqueued during the drain), then one macrotask is taken from the macrotask queue, then the microtask queue drains again, and so on.
async/await participates in this model as syntactic sugar over Promises. Code before the first await in an async function runs synchronously. Code after an await is scheduled as a microtask when the awaited Promise settles. This means it runs after all other currently queued synchronous code but still before any pending setTimeout callbacks.
The single most important rule to memorize for interviews: microtasks drain completely before the next macrotask, and this draining includes any microtasks that are enqueued during the drain itself. A chain of ten .then callbacks will all run before a single setTimeout(fn, 0) callback.
Can setTimeout(fn, 0) execute before Promise callbacks?
No. Promise microtasks always run before macrotasks like setTimeout, regardless of delay.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement