Q007: Microtasks vs Macrotasks
What Interviewers Want To Evaluate
This question tests whether you can explain asynchronous ordering without hand-waving. Interviewers want to see whether you understand where promises, timers, user events, queueMicrotask, rendering, and the call stack fit together.
For senior roles, the answer should move beyond "promise before timeout." You should explain why microtasks have priority, how microtask starvation can delay rendering, and how this affects UI responsiveness in real applications.
Short Interview Answer
Macrotasks, usually called tasks in browser specifications, include work such as initial script execution, timers, user events, and message events. Microtasks include promise callbacks, queueMicrotask, and mutation observer callbacks. After the current task finishes and the call stack is empty, the browser drains the microtask queue before it moves to the next task and before it gets a rendering opportunity. This is why promise callbacks run before setTimeout(..., 0), but also why too many microtasks can delay paint and input handling.
Detailed Interview Answer
The browser event loop works in cycles. It picks a task, runs JavaScript until the stack is empty, drains all available microtasks, gives the browser a chance to render, and then moves to the next task. A task is a larger unit of scheduled work. A microtask is a smaller, high-priority continuation that runs immediately after the current stack finishes.
Promises use microtasks because promise continuations should run soon after the current synchronous work completes, before unrelated future tasks. Timers use the task queue, so even a zero-delay timer waits until the current task and all queued microtasks finish.
This priority is useful, but it can be dangerous. If each microtask schedules another microtask, the browser may keep draining microtasks and delay rendering. In production, that can appear as a frozen UI even though the code is technically "async."
Deep Technical Explanation
The simplified execution order is:
Current task starts
-> synchronous JavaScript runs
-> call stack becomes empty
-> microtask queue drains completely
-> browser may render
-> next task starts
Consider this example:
console.log("start");
setTimeout(() => console.log("timer"), 0);
queueMicrotask(() => console.log("microtask"));
Promise.resolve().then(() => console.log("promise"));
console.log("end");
The output is:
start
end
microtask
promise
timer
The synchronous logs run during the current task. Both queueMicrotask and the promise callback enter the microtask queue. The timer enters the task queue. Microtasks drain before the timer task gets a turn.
Internal Working
The exact queues and task sources are more nuanced in the browser specification, but the senior-level model is enough for most frontend work: tasks are event-loop turns, microtasks are high-priority continuations after the current stack, and rendering happens only when the browser gets control back.
Node.js has related but different phases. For browser interviews, be explicit that you are describing browser behavior unless asked about Node.
Architecture Discussion
Microtasks matter in frontend architecture because frameworks, state libraries, data clients, and promise-heavy code all schedule work. If an application chains many promise continuations after a user action, it can delay rendering even though it appears to be broken into async pieces.
For a React application, this matters around data fetching, state batching, hydration, transitions, and post-event work. If a user click triggers a promise-heavy chain that computes large derived data, the next paint may be delayed.
Trade-Offs
Microtasks are useful when you need to run cleanup or continuation logic before the browser handles the next task. Tasks are useful when you intentionally want to yield and let the browser process rendering or input. Workers are useful when the work itself is CPU-heavy and should not compete with UI rendering on the main thread.
Choosing the queue is a scheduling decision. Use microtasks for short follow-up work. Use tasks or scheduling APIs when the UI needs breathing room.
Common Mistakes
A common mistake is treating "macrotask" and "task" as two completely different ideas. In browser specs, the term is generally task; macrotask is common interview vocabulary used to contrast with microtask.
Another mistake is assuming await automatically lets the browser paint. An await continuation is promise-based, so it resumes through microtask scheduling. A long chain of await continuations can still delay rendering.
Real Production Story
In a data-heavy UI, a developer may refactor synchronous logic into many promises and expect the UI to become responsive. But if the promise chain immediately schedules more microtasks, the browser still does not get a rendering opportunity. The performance trace shows continuous scripting and delayed paint.
The fix is to yield to a task, chunk the work, use requestAnimationFrame for visual coordination, use scheduler.postTask where available, or move CPU-heavy processing to a worker.
Enterprise Example
In a workflow automation tool, saving a form may trigger validation, dependency checks, audit logging, and optimistic UI updates. Short promise continuations are fine. But if validation expands into thousands of microtasks, the UI can feel locked. A better design separates immediate user feedback from heavier validation and schedules noncritical work outside the critical interaction path.
Code Example
export function yieldToBrowser() {
return new Promise<void>((resolve) => {
setTimeout(resolve, 0);
});
}
export async function processInBatches<T>(
items: T[],
processItem: (item: T) => void,
batchSize = 250,
) {
for (let index = 0; index < items.length; index += batchSize) {
const batch = items.slice(index, index + batchSize);
for (const item of batch) {
processItem(item);
}
await yieldToBrowser();
}
}
This intentionally yields through a task boundary between batches. It is not the only scheduling approach, but it demonstrates the difference between promise-only continuation and giving the browser a chance to handle other work.
Performance Discussion
Microtask starvation can delay paint. Long tasks can delay input. Promise-heavy code can create large scripting blocks. Use DevTools to inspect whether work is broken across frames or whether the main thread stays continuously busy.
When optimizing, measure the interaction. Look at scripting duration, rendering opportunities, input delay, and whether chunking actually improves responsiveness on lower-end devices.
Security Considerations
Untrusted input can trigger event-loop abuse if it causes unbounded promise chains, recursive microtasks, or expensive scheduling. Validate limits and avoid designs where user-controlled data can create unbounded queues.
Scalability Discussion
As data sizes grow, scheduling becomes part of scalability. A small list can be processed in one task. A large list may need chunking, cancellation, prioritization, virtualization, server-side processing, or workers. Microtasks are not a scalability strategy for heavy work.
Senior Engineer Perspective
A senior engineer should explain task ordering and then connect it to UI behavior. The strongest answers say when microtasks are appropriate and when yielding to a task or worker is better.
Lead Engineer Perspective
A lead engineer should turn this into team guidance: avoid unbounded microtask chains, profile promise-heavy interactions, define worker patterns, and teach engineers to measure responsiveness rather than trust async-looking code.
Whiteboard Explanation
Draw three areas: call stack, microtask queue, and task queue. Put promises and queueMicrotask in the microtask queue. Put timers and user events in the task queue. Show microtasks draining before the next task and before rendering.
Revision Notes
Microtasks run after the current stack empties and before the next task. Tasks include timers and user events. Promise callbacks are microtasks. Timers are tasks. Too many microtasks can delay rendering.
Key Takeaways
Microtasks are high-priority continuations, not background threads. They are powerful for ordering but dangerous when abused. Responsiveness often requires yielding to the browser, not merely using promises.
Related Topics
- Event loop
- Call stack
- Promises
queueMicrotasksetTimeout- Rendering pipeline
- Long tasks
- Web workers
Practice Exercise
Use the JavaScript practice ground to create examples with console.log, promises, queueMicrotask, and setTimeout. Predict the output before running it. Then create a promise chain that schedules many microtasks and explain why it can delay rendering.
Follow-Up Questions
- What is a task?
- What is a microtask?
- Why do promise callbacks run before timers?
- Can microtasks starve rendering?
- Does
awaitguarantee a paint? - Is
setTimeout(..., 0)immediate? - How is browser behavior different from Node.js behavior?
- When should you yield through a task?
- When should work move to a worker?
- How would you debug a promise-heavy interaction?
Things Interviewers Hate Hearing
"Promises are faster than timers" misses the point. The important difference is scheduling priority and event-loop ordering, not a generic speed claim.
How I Would Answer This In A Real Interview
I would say tasks are larger event-loop units like script execution, timers, and user events, while microtasks are high-priority continuations like promise callbacks and queueMicrotask. After the current task finishes and the stack is empty, the browser drains microtasks before moving to the next task and before it may render.
Then I would add the production angle. This is why promise callbacks run before timers, but also why promise-heavy code can still make the UI feel frozen. If we need responsiveness, we may need to yield through task boundaries, chunk work, use rendering-aware scheduling, or move CPU-heavy work to a web worker.