skip to content

You are designing a JavaScript library that coalesces many small updates into one flush. How do you decide between flushing at the microtask checkpoint (queueMicrotask or Promise.resolve().then) and flushing on a fresh task, and what does each choice cost?

level: principalimportance: should knowfreq 24%

answer

  1. who may observe the intermediate state
  2. microtask stays inside this turn
  3. task hands control back
  4. unbounded flush needs a boundary
  5. atomicity versus responsiveness

basics

~20 s

Flush at the microtask checkpoint when the batch must settle before anything outside your library can observe it; flush on a task when the runtime needs control back first. Microtasks extend the current turn; tasks let unrelated work interleave.

solid answer

~50 s

The decision turns on one question: may anything else observe the system between the update and the flush? A microtask flush runs at the checkpoint after the current synchronous run, before the runtime regains control, so no other task, event, or host work can see the intermediate state — the batch is atomic from the outside. The price is that the flush is part of the current turn: it is unbounded, unpreemptible, and it delays everything the runtime wanted to do next, which matters if the flush is expensive or scales with input. A task flush inverts both properties: the runtime gets control back immediately, so other work proceeds and the turn stays short, but consumers can observe the un-flushed state in between and your batch can be delayed by whatever the runtime chooses to do first. Cheap, latency-critical coalescing goes on the microtask queue; expensive or best-effort work goes on a task.

go deeper

for a junior

Know the basic difference: a microtask flush runs right after the current code finishes, while a task flush waits for the runtime to come back around. Recognising that both coalesce many updates into one is enough here.

for a middle

Explain the mechanics behind each option — the checkpoint drains to empty inside the current turn, whereas scheduling a task returns control to the runtime first — and name one concrete consequence of each.

for a senior

Demonstrate judgment about cost: an unbounded flush on the microtask queue stretches the turn with no way to stop, and you should be able to describe how that appears in a profile and how you would bound it.

for a principal

Own it as an API contract, not a scheduling detail: decide whether your library guarantees nobody observes intermediate state, justify that guarantee against flush cost and composition with other libraries, and design the read path and escape hatches to match.

## Frame the decision correctly Both options coalesce: you mark work as pending, schedule one flush, and let further updates in the same window join the existing batch. What differs is *when* control returns to the runtime relative to your flush, and that single difference drives every consequence worth arguing about. - **Microtask flush** (`queueMicrotask(flush)` or `Promise.resolve().then(flush)`): the flush runs at the checkpoint after the current run-to-completion finishes and before the runtime takes another task. The whole microtask queue drains to empty first, so your flush plus anything it enqueues completes inside the current turn. - **Task flush** (scheduling a fresh task): the runtime regains control immediately after your synchronous code returns, does whatever it does between tasks, and reaches your flush later. ## Argument for the microtask flush: observational atomicity With a microtask flush, no code outside the current turn can ever see a half-updated system. The sequence "caller mutates state N times, library flushes once" is indivisible from the perspective of every event handler, every callback, and every piece of host work, because none of them run until the drain is over. That is a strong contract: consumers of your library never need to ask "is a flush pending?", and you never have to expose a `flushSync()` escape hatch for people who got caught between the two states. It is also the lowest-latency option that is still asynchronous. The batch settles at the earliest moment that is not synchronous — you paid one checkpoint, not one full turn. ## Argument against it: the turn has no budget The drain is exhaustive and cannot be pre-empted. Whatever your flush costs is added directly to the current turn, and if the flush enqueues more microtasks, those run in the same drain too. Three failure shapes follow: 1. **Cost that scales with input.** A flush that is O(pending) is fine at ten updates and pathological at a hundred thousand. The mechanism gives you no place to stop halfway. 2. **Latency you cannot see in isolation.** Your library's flush is invisible in its own micro-benchmark and shows up as a long turn in an application profile, attributed to whatever ran first in that turn. 3. **No cooperation with other libraries.** Every microtask-flushing library in the process piles into the same drain, so the tail is the sum of all of them. ## Argument for the task flush: handing control back Scheduling a task ends your involvement in the current turn. The runtime does its own work, other pending tasks run, and the flush happens later. Any work that is genuinely deferrable — persistence, analytics, recomputing a derived cache, anything the user is not waiting on — belongs here, because the cost of a slightly stale view is far lower than the cost of a stretched turn. The cost is symmetrical to the benefit. Between the update and the flush, arbitrary code runs and can observe the un-flushed state, so "read your own writes" is no longer free — you have to define whether reads see pending updates, which usually means either reading through a pending-aware accessor or exposing an explicit synchronous flush. And your flush's actual delay is decided by the runtime's scheduling, not by you. ## The questions I would actually ask before choosing 1. **Does anything outside the library read this state?** If yes, and staleness is a correctness problem rather than a cosmetic one, the microtask flush removes the whole question. 2. **Is the flush bounded and cheap?** If its cost scales with the batch and the batch is user-controlled, the microtask queue is the wrong home — you have handed callers a way to extend a turn arbitrarily. 3. **Is anyone waiting on the result?** Work on the critical path of something a user perceives argues for the checkpoint; background reconciliation argues for a task. 4. **How does it compose with itself?** If a flush can trigger more updates, a microtask flush re-enters within the same drain and needs an explicit stop condition. A task flush gives you a natural boundary between rounds. 5. **What is the failure mode of being wrong?** Wrong microtask choice degrades responsiveness under load, diffusely and hard to attribute. Wrong task choice produces visible staleness or ordering bugs, which are easier to spot and cheaper to fix. ## A defensible default Small, bounded coalescing that other code reads → flush at the microtask checkpoint, and document that reads after the current run see a settled system. Expensive, unbounded, or best-effort work → flush on a task, and give callers an explicit way to force it when they need consistency now. Hybrid designs exist — coalesce state at the checkpoint, defer the expensive derived work to a task — and are often the right answer for a library that owns both a cheap authoritative update and an expensive projection of it. What makes this a senior-plus answer is refusing to treat it as a performance micro-choice. It is a contract decision: you are choosing whether your library guarantees that nobody ever observes the intermediate state, and everything else follows from that guarantee.

  • If you flush at the microtask checkpoint, what should happen when the flush itself produces new pending updates?
    Define an explicit stop condition. A flush that schedules another flush lands in the same drain, so unbounded rounds never hand control back. Either process to a fixed point with a round cap and report when the cap is hit, or let the first round settle and push subsequent rounds onto a task so each round gets its own turn.
  • How would you let callers get a consistent read when the library flushes on a task?
    Either make reads pending-aware — the accessor merges queued updates so callers always see their own writes — or expose an explicit synchronous flush they call before reading. The pending-aware read keeps the API honest by default; the explicit flush is simpler but pushes the correctness burden onto every caller who forgets it.
  • Two libraries in the same application both flush at the microtask checkpoint. What does that mean for the turn?
    Their flushes drain together, so the turn's length is the sum of both plus anything they enqueue, and neither can yield to the other. This is why unbounded microtask flushes are antisocial in a shared process: each library looks fine in isolation and the composed application shows long, hard-to-attribute turns.

saying these in an interview costs you the question

  • Treats the choice as pure performance, not a visibility contract
  • Assumes a microtask flush is always faster and therefore better
  • Puts unbounded work on the microtask queue without a stop condition
  • Thinks a task flush still prevents others from seeing stale state
  • Adds a synchronous flush escape hatch instead of defining the contract

context