Skip to main content
Guides Skills and frameworks JavaScript Event Loop Interview Guide — Microtasks, Macrotasks, and Async Timing
Skills and frameworks

JavaScript Event Loop Interview Guide — Microtasks, Macrotasks, and Async Timing

9 min read · April 25, 2026

A practical JavaScript event loop interview guide explaining call stack order, microtasks, macrotasks, async/await timing, rendering, and common output questions.

JavaScript Event Loop Interview Guide — Microtasks, Macrotasks, and Async Timing

A JavaScript event loop interview guide has one job: help you predict async timing without guessing. The key distinction is microtasks versus macrotasks. Synchronous code runs first on the call stack. Then the JavaScript runtime drains the microtask queue, usually including promise callbacks and queueMicrotask. Then it runs the next macrotask, such as setTimeout, UI events, or message callbacks. If you can explain that sequence clearly, most event-loop questions become mechanical.

JavaScript event loop interview guide: the core model

JavaScript execution is single-threaded from the perspective of your code: one call stack runs one piece of JavaScript at a time. The browser or Node runtime can do work outside that stack, such as timers, network I/O, file I/O, or rendering coordination. When that work is ready to notify JavaScript, it schedules callbacks into queues.

The simplified browser model is:

  1. Run the current synchronous script to completion.
  2. Drain the microtask queue until it is empty.
  3. Optionally render/update the page.
  4. Run the next macrotask from the task queue.
  5. Repeat.

That "drain microtasks until empty" detail matters. If a microtask schedules another microtask, the new one runs before the browser moves on to a timer or click handler. This is why promises often beat setTimeout(..., 0).

Call stack, web APIs, queues, and rendering

A clear interview answer names the pieces but does not overcomplicate them:

  • Call stack: where currently executing functions live. Synchronous code runs here.
  • Web APIs / runtime APIs: timers, fetch, DOM events, and other host-provided operations that can complete later.
  • Macrotask queue: callbacks for timers, events, message channels, script execution, and other tasks.
  • Microtask queue: promise reactions, queueMicrotask, and mutation observer callbacks in browsers.
  • Render step: browsers generally get a chance to update layout/paint after microtasks and before the next task.

The event loop is not a magical parallel executor. It is a coordinator that decides what callback gets pushed onto the call stack next, once the stack is empty.

The classic ordering example

Consider this code:

console.log('A');

setTimeout(() => console.log('B'), 0);

Promise.resolve().then(() => console.log('C'));

console.log('D');

The output is:

A
D
C
B

Why? A logs during synchronous script execution. setTimeout schedules a timer callback as a macrotask. The promise .then schedules a microtask. D logs synchronously. After the script finishes, the stack is empty, so the runtime drains microtasks: C. Only then does it take the timer task: B.

In interviews, narrate the scheduling moment and the execution moment separately. Saying "the promise runs immediately" is sloppy. The promise may already be resolved, but the .then callback still runs asynchronously as a microtask after the current stack completes.

Async/await timing without confusion

async/await is promise syntax. An async function starts synchronously until it hits an await. After that, the continuation is scheduled like a promise reaction.

Example:

async function run() {
  console.log('1');
  await Promise.resolve();
  console.log('2');
}

console.log('3');
run();
console.log('4');

Output:

3
1
4
2

run() starts immediately and logs 1. At await, it yields; the rest of the function continues in a microtask. Control returns to the outer script, which logs 4. After synchronous code completes, the microtask logs 2.

A good phrasing: "Code before the first await runs synchronously. Code after await resumes in a future microtask, even if the awaited promise is already resolved."

Microtasks vs macrotasks in practical terms

| Mechanism | Queue type | Typical use | Interview note | |---|---|---|---| | Promise.then/catch/finally | Microtask | Promise reaction | Runs before timers after current stack | | queueMicrotask | Microtask | Explicit microtask scheduling | Can starve rendering if abused | | MutationObserver | Microtask-ish browser callback | DOM mutation notifications | Runs before next task/render checkpoint | | setTimeout | Macrotask | Timer callback | Minimum delay is not guaranteed exact timing | | setInterval | Macrotask | Repeated timer | Delays can drift if stack is busy | | DOM event | Macrotask | User interaction | Handler runs when event task is processed | | requestAnimationFrame | Render-aligned callback | Before paint | Not the same as microtask or normal timer |

Use "task" or "macrotask" carefully. Many browser specs say "task" rather than macrotask, but interviews usually use microtask/macrotask language. It is fine to say "what people call a macrotask."

Why microtasks can starve timers and rendering

Because the runtime drains the microtask queue until empty, an infinite or very long chain of microtasks can prevent timers, user input, or rendering from happening promptly.

function loop() {
  Promise.resolve().then(loop);
}
loop();
setTimeout(() => console.log('timer'), 0);

The timer may never run because each microtask schedules another microtask before the event loop can move to the timer queue. In real applications, this shows up as frozen UI, delayed input, or a page that cannot paint. The fix is to yield to a task or frame boundary when doing large chunks of work, using techniques such as chunking with setTimeout, scheduler.postTask where available, or requestAnimationFrame for visual work.

This is a strong senior-level point: promises are not always "more efficient" if you create unbounded microtask chains.

Rendering and requestAnimationFrame

In browsers, rendering generally happens between tasks after microtasks have been drained. requestAnimationFrame callbacks are scheduled before the next paint, which makes them appropriate for animation reads/writes.

Interview example:

button.addEventListener('click', () => {
  box.style.width = '200px';
  Promise.resolve().then(() => {
    box.textContent = 'updated';
  });
  requestAnimationFrame(() => {
    console.log('before paint');
  });
});

The click handler is a task. The promise callback runs as a microtask after the handler. The requestAnimationFrame callback runs before a paint. Exact browser details can be nuanced, but the useful interview point is that microtasks run before the browser gets a normal chance to render, and requestAnimationFrame is for coordinating with paint.

Node.js caveat: process.nextTick and phases

If the interview is browser-focused, do not dive too deeply into Node. But if Node comes up, mention that Node has event-loop phases for timers, pending callbacks, poll, check, and close callbacks. It also has process.nextTick, which is even higher priority than normal promise microtasks in Node's scheduling model.

A practical hierarchy in Node interview questions is often:

  1. Synchronous code.
  2. process.nextTick callbacks.
  3. Promise microtasks.
  4. Timers / immediates depending on phase and context.

Do not overstate universal ordering between setTimeout(..., 0) and setImmediate() because it can depend on where they are scheduled. A careful answer beats a memorized one.

Common event-loop output questions

When asked "what logs first?" use this annotation process:

  1. Mark all synchronous logs.
  2. For each async call, write down which queue receives the callback.
  3. Remember that promise reactions and await continuations are microtasks.
  4. Finish all synchronous code.
  5. Drain microtasks in order, including microtasks created by microtasks.
  6. Run the next task.

Example:

console.log('start');

setTimeout(() => console.log('timeout'), 0);

Promise.resolve()
  .then(() => {
    console.log('p1');
    return Promise.resolve();
  })
  .then(() => console.log('p2'));

queueMicrotask(() => console.log('micro'));

console.log('end');

Output:

start
end
p1
micro
p2
timeout

p1 is queued before micro. The second .then is not queued until the first .then resolves, so it lands after the already queued micro.

Practical bugs explained by the event loop

Event-loop knowledge is useful outside trivia questions:

  • UI freezes: long synchronous work blocks the call stack, so no input, timers, or rendering can proceed.
  • Unexpected stale DOM reads: DOM changes may not be painted yet; coordinate with requestAnimationFrame if visual timing matters.
  • Timer delays: setTimeout(fn, 0) means run after at least the current task and microtasks, not immediately.
  • Race conditions: multiple async sources update shared state in an order you did not expect.
  • Unhandled promise errors: promise rejections are reported asynchronously; attach handlers intentionally.

A strong candidate connects the event loop to performance and correctness, not just console-log puzzles.

How to explain it in an interview

Use this answer template:

"JavaScript runs one call stack at a time. Synchronous code runs to completion first. Async APIs schedule callbacks. Promise callbacks and await continuations go to the microtask queue, while timers and many events are tasks/macrotasks. After the current stack is empty, the runtime drains all microtasks before moving to the next task, and the browser may render between tasks. That is why promise callbacks usually run before setTimeout(..., 0), and why long synchronous work or unbounded microtasks can block the UI."

Then walk the code line by line. Interviewers want calm, deterministic reasoning.

Prep checklist

Make sure you can explain or solve:

  • Why Promise.then logs before setTimeout.
  • What part of an async function runs synchronously.
  • What happens when a microtask schedules another microtask.
  • Why setTimeout(..., 0) is not immediate.
  • How rendering relates to tasks, microtasks, and requestAnimationFrame.
  • Why long loops block UI even when timers are scheduled.
  • Node-specific ordering of process.nextTick, promises, timers, and setImmediate at a high level.

The event loop is easiest when you treat it as a queueing problem. Finish the current stack, drain microtasks, then take the next task. Keep that loop in your head and async timing questions stop feeling like magic.

A step-by-step trace template

For harder event-loop snippets, draw a tiny table while you reason. Columns should be: current line, stack action, microtask queue, task queue, and output. You do not need a perfect spec diagram; you need a disciplined trace.

Example interview narration:

console.log('one');
setTimeout(() => console.log('two'), 0);
Promise.resolve().then(() => {
  console.log('three');
  setTimeout(() => console.log('four'), 0);
});
Promise.resolve().then(() => console.log('five'));
console.log('six');

Walk it this way: one logs synchronously. The first timeout schedules a task. The first promise schedules a microtask. The second promise schedules another microtask. six logs synchronously. Now the stack is empty, so the first microtask logs three and schedules a new timer task. The second microtask logs five. Only after the microtask queue is empty does the runtime process timer tasks, so two logs before four because its timer was queued first.

The output is:

one
six
three
five
two
four

This style prevents a common mistake: treating every nested async callback as if it jumps to the front. It does not. A callback's queue and enqueue time both matter.

What not to say

Avoid saying "JavaScript is asynchronous" as if all code is async. JavaScript execution is synchronous unless you call an API that schedules later work. Avoid saying "promises run in parallel." Promise callbacks are queued; they do not run on a second JavaScript call stack. Avoid saying "zero-millisecond timeout runs immediately." It runs in a future task after the current script and microtasks. These small wording differences matter because event-loop interviews are really precision interviews.