Explain the Event Loop and Execution Order in JavaScript

Intermediate15 min interview
Skills tested:
Predicting output order for mixed synchronous, Promise, and setTimeout codeUnderstanding that microtasks drain completely before the next macrotaskTracing async/await execution through the event loop modelUnderstanding why nested Promise chains still run before setTimeoutExplaining what "blocking the event loop" means and its effects

Advertisement

🧩 Scenario

In a real codebase, incorrect mental models of the event loop cause subtle bugs: state updates that appear in the wrong order, UI that does not reflect the latest data, and animations that stutter because synchronous code blocks the rendering step. Predicting execution order correctly is also one of the most common JavaScript interview exercises at mid and senior levels.

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 typeQueueWhen it runs
Synchronous codeCall stackImmediately, blocking all other execution
Promise .then / awaitMicrotask queueAfter current task, fully drained before next macrotask
setTimeout / setIntervalMacrotask queueOne 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 await does to the current async function without saying "it pauses JavaScript"
  • Whether you know that code after await resumes as a microtask, not synchronously
  • Whether you can describe the consequence of a large synchronous operation inside an async function

Follow-Up Questions

  1. You have five await null calls in a row inside an async function. How many times does the event loop yield before the last line runs?
  2. What is the difference between Promise.resolve().then(fn) and queueMicrotask(fn)?
  3. If an async function throws synchronously before its first await, when does the catch handler run?
  4. How does process.nextTick in Node.js fit into this model relative to Promise microtasks?
  5. 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 await pauses the thread or JavaScript engine when it only suspends the current async function
  • Not knowing that code after await resumes as a microtask and can be interleaved with other microtasks
  • Predicting that a chained .then inside another .then runs after the next setTimeout when 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 before setTimeout(fn, 0)?
  • Can you explain what await does 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.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

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