skip to content

In modern JavaScript, how does queueMicrotask(fn) differ from Promise.resolve().then(fn), given that both run fn at the next microtask checkpoint?

level: middleimportance: should knowfreq 42%

answer

  1. same queue, same order
  2. one returns a promise, one returns nothing
  3. throw lands somewhere different
  4. rejection channel can go unwatched
  5. try/catch at the call site catches neither

basics

~20 s

Both run fn at the same microtask checkpoint, in enqueue order. queueMicrotask allocates no promise and returns nothing to chain, and a throw inside it surfaces as an uncaught error rather than becoming a promise rejection that can be silently swallowed.

solid answer

~40 s

They target the same queue and the same checkpoint, so ordering between them is simply enqueue order. The differences are in cost and error routing. `Promise.resolve().then(fn)` allocates two promises — the resolved one and the derived one `.then` returns — and wraps `fn`'s outcome: a value fulfils the derived promise, a throw *rejects* it, and if nobody attached a `.catch`, that failure only shows up through the unhandled-rejection channel, which is easy to miss. `queueMicrotask(fn)` allocates nothing, returns `undefined`, and lets a synchronous throw propagate as an uncaught exception reported to the host, the same way a throw from a timer callback would. So use `queueMicrotask` when you just want deferred work with loud failures, and the promise form when you actually need the resulting promise to compose with.

code

javascript · 13 lines
javascript
// Same queue, plain enqueue order
queueMicrotask(() => console.log(1));
Promise.resolve().then(() => console.log(2));
queueMicrotask(() => console.log(3));
// 1, 2, 3

// Different error routing
queueMicrotask(() => { throw new Error('loud'); });
// -> reported as an uncaught error by the host

Promise.resolve().then(() => { throw new Error('quiet'); });
// -> rejects the derived promise; nobody attached .catch,
//    so it only reaches the unhandled-rejection channel

go deeper

for a junior

Know that both schedule a callback for the next microtask checkpoint and that neither runs immediately. Being able to say they share one queue is enough at this level.

for a middle

Explain the concrete differences: promise allocation, whether you get a chainable handle back, and the fact that a throw rejects the derived promise in one case and surfaces as an uncaught error in the other.

for a senior

Show the incident angle — fire-and-forget promise work whose rejection goes to an unmonitored channel leaves the system in a half-updated state silently, and you should say how you make such failures observable in a real service.

for a principal

Own the API-design call: whether your library's deferral primitive should hand callers a promise they must handle or fail loudly by default, and what that choice implies for the error-reporting contract across a codebase.

## Same queue, same checkpoint Start with what is *not* different. `queueMicrotask(fn)` and `Promise.resolve().then(fn)` both place a job on the microtask queue, and both therefore run after the current synchronous run completes and before the next task. Interleave the two and they run strictly in the order they were enqueued — there is no priority split between promise jobs and `queueMicrotask` jobs. If a candidate claims one class of microtask outranks the other, that is the misconception to correct. ``` queueMicrotask(() => console.log(1)); Promise.resolve().then(() => console.log(2)); queueMicrotask(() => console.log(3)); // 1, 2, 3 ``` ## Difference 1: what gets allocated `Promise.resolve()` creates a promise. `.then(fn)` creates a second one — the derived promise it returns — plus a reaction record. `queueMicrotask(fn)` creates none of that; it takes the function and enqueues it. For one-off deferral the allocation difference is irrelevant, but in a hot path that defers work thousands of times per second it is real garbage you are asking the collector to clean up for no benefit, since nobody was going to use the returned promise. ## Difference 2: what you get back `.then` returns a promise, so the deferred work composes: you can chain more steps, `await` it, pass it to a combinator, or attach a `.catch`. `queueMicrotask` returns `undefined` — there is no handle, no way to know when the callback ran, and no way to cancel it. If the deferred work is the beginning of an asynchronous flow the caller cares about, the promise form is the right one precisely because it gives you that handle. ## Difference 3: where a throw goes — the one that bites This is the substantive difference. A `throw` inside a `.then` handler does not escape. It **rejects the derived promise**. If the calling code ignored the returned promise, that rejection has no handler, and the failure surfaces only through the runtime's unhandled-rejection reporting — an `unhandledrejection` event in browsers, a process-level warning or crash in server runtimes depending on configuration. Teams routinely have that channel unmonitored, so the error is effectively swallowed while the program carries on in a half-updated state. A `throw` inside a `queueMicrotask` callback has no promise to land in. It propagates out of the job as an uncaught exception and is reported to the host the same way an exception from a timer callback is — a global error report. It is loud by default. Neither form can be caught by a `try/catch` around the *scheduling* call. By the time the callback runs, that stack frame is long gone; the `try` block completed before the checkpoint even happened. That trips up more candidates than the routing difference itself. ``` try { queueMicrotask(() => { throw new Error('boom'); }); } catch (e) { // never reached — the job runs later, on an empty stack } ``` The practical guidance: if you deliberately schedule fire-and-forget work, `queueMicrotask` fails loudly, which is usually what you want for a bug. If you use the promise form for fire-and-forget, add a `.catch` that reports, or the failure goes to a channel nobody is reading. ## Difference 4: layering Promises are ECMAScript. `queueMicrotask` is a **host** API — a global provided by the browser and by server runtimes, not by the language core. In practice it is available everywhere modern code runs (browsers since the late 2010s, Node since version 11), but it is the reason you occasionally see `Promise.resolve().then(fn)` used as the deferral primitive inside libraries that predate it or that target unusual embeddings without host globals. ## Choosing between them - Need the deferred work to compose, be awaited, or report failure through a promise chain → `Promise.resolve().then(fn)`, and handle the rejection. - Need only "run this after the current run finishes", with no handle and loud failures → `queueMicrotask(fn)`, and put a `try/catch` **inside** the callback if you want to control the reporting. - Need the runtime to get control back so other work can proceed → neither. Both keep you inside the current turn's drain; that requires scheduling a task instead. ## What the interviewer is testing The surface answer, "they're the same thing", is half right and stops before the interesting part. The full answer names the shared queue and ordering first, then separates cost, composability, and — the one that causes production incidents — the fact that one route turns an exception into a possibly-unobserved rejection while the other lets it surface as an uncaught error.

  • Can you catch a throw from either callback with a try/catch wrapped around the scheduling call?
    No. Both callbacks run at the microtask checkpoint, long after the `try` block finished and its frame left the stack. A `try/catch` only ever guards the enqueue itself. To handle the failure you put the `try/catch` inside the callback, or attach a `.catch` to the promise the `.then` form returns.
  • If both are enqueued in the same run, which callback runs first?
    Whichever was enqueued first. There is one microtask queue and it is FIFO; promise reaction jobs get no priority over `queueMicrotask` callbacks or vice versa. Interleaving the two and expecting a category-based ordering is a common wrong prediction.
  • Why do some libraries still defer with Promise.resolve().then(fn) instead of queueMicrotask?
    Because `queueMicrotask` is a host global rather than part of the language, so very old environments or minimal embeddings may not provide it, while promises are guaranteed by the language itself. Code written before it was widely available used the promise trick as a portable microtask primitive and often still does.

saying these in an interview costs you the question

  • Says queueMicrotask callbacks run before promise reactions
  • Thinks a try/catch around the call catches the callback's throw
  • Believes a throw in a .then handler crashes the program
  • Claims queueMicrotask returns a promise you can await
  • Treats queueMicrotask as part of the ECMAScript language

context