cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

The microtask queue

Learn where promise jobs wait, how hosts run microtask checkpoints, why queues can starve timers, and how queueMicrotask compares.

By the end, you can
  • 01
    Name the boundaryExplain which promise rules ECMAScript defines and which queue and checkpoint choices belong to a host.
  • 02
    Trace a checkpointPredict task and microtask output by draining every ready microtask after one task finishes.
  • 03
    Avoid starvationRecognize recursive microtasks and choose when a later task is a healthier yield point.

A small queue with a big effect

A browser or Node program does not run every ready callback at the same moment. It finishes the current JavaScript work, then chooses what is allowed to run next. Promise handlers are part of a special queue called the microtask queue.

Definition

A microtask is small queued work that a host runs after the current JavaScript task has finished, before it chooses the next task.

One script, one promise, one timerPop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("timer"), 0);Promise.resolve().then(() => console.log("promise"));console.log("script");

Line 1 asks the host to run a timer later. Line 2 makes a ready promise reaction, which becomes a microtask. Line 3 logs immediately. The real output is script, then promise, then timer.

The everyday Microtasks lesson introduces that order. This lesson asks where promise jobs wait and why a host drains them before moving to a timer, event, or other task.

Job queues in the specification

A job is specification language for work that must happen later. ECMAScript says promise reactions are queued and run later in order. It does not own a browser tab, Node event loop, screen, or timer implementation.

A ready promise job waitsPop out in the code editor (opens in a new tab)JavaScript
Promise.resolve("tea").then(() => console.log("job"));console.log("task");console.log("task still running");

Line 1 creates a ready reaction job. Line 2 logs while the current task still owns the stack. Line 3 proves that the task continues. The output is task, task still running, then job.

That split is useful. JavaScript can promise that a then handler is not called inline, while browsers and Node remain free to fit jobs into their own surrounding event loops. The exact queue data structure is host implementation detail.

Think in two layers when debugging. The language tells you that ready promise work is later work. The host tells you when the current task ends and a microtask checkpoint begins.

Two lines prove a promise job is laterPop out in the code editor (opens in a new tab)JavaScript
Promise.resolve("tea").then(() => console.log("job"));console.log("task");console.log("task still running");

Line 1 registers the reaction on an already fulfilled promise. Line 2 runs in the task that is already on the stack. Line 3 is still ordinary current-task work. The output is task, task still running, then job.

The word job is deliberately broad. It names a piece of later language-level work, not a visible browser queue widget. When a browser changes its renderer or Node changes its I/O machinery, the promise rule can stay the same.

HostEnqueuePromiseJob

HostEnqueuePromiseJob is the named hook the promise algorithms use to hand a ready promise job to the host. It is not an API in your program. It is the specification boundary between promise rules and a browser or Node queue.

A promise handler reaches the host queuePop out in the code editor (opens in a new tab)JavaScript
const order = Promise.resolve("tea");order.then(() => console.log("serve tea"));console.log("take next order");

Line 1 creates a fulfilled promise. Line 2 registers a handler, so the promise algorithms arrange a later job through the host hook. Line 3 runs before that job. The output is take next order, then serve tea.

The name helps you avoid a common mistake. Promise behavior is specified by ECMAScript, but no JavaScript source line reaches inside Chrome or Node and pushes onto an implementation queue. Your code asks for promise behavior; the host receives the job.

This is why documentation sometimes sounds different across runtimes while basic promise order stays familiar. The shared rule is later promise work. The surrounding task and rendering details belong to the particular host.

A tiny model of the host hookJavaScript
const hostMicrotasks = []; function HostEnqueuePromiseJob(job) {  hostMicrotasks.push(job);} HostEnqueuePromiseJob(() => console.log("serve tea"));console.log("take next order");hostMicrotasks.shift()();

Line 1 is a plain array standing in for host-owned microtask storage. Lines 3 through 5 define the teaching version of the hook: it receives a job and stores it. Line 7 hands over the tea job. Line 8 still prints first, and line 9 drains the one stored job.

This is not the browser's source code. It is a boundary model: promise algorithms can request that work be queued without ECMAScript deciding how a host represents the queue or when its event loop selects tasks.

Microtask checkpoints

A microtask checkpoint is the host moment that drains the microtask queue until it is empty. A useful teaching model is: run one task, let the JavaScript stack become empty, then drain every microtask before taking the next task.

A microtask can queue another microtaskPop out in the code editor (opens in a new tab)JavaScript
queueMicrotask(() => {  console.log("first");  queueMicrotask(() => console.log("second"));});console.log("script");

Lines 1 through 4 queue a callback that queues another callback. Line 5 logs immediately. At the checkpoint, the first callback runs and adds the second. The real output is script, first, then second.

In browsers, hosts also perform checkpoints around event dispatch and rendering opportunities. For example, separate user event listeners can have a checkpoint between them. Treat rendering timing as browser behavior, not as an extra ECMAScript promise rule.

A listener model with a checkpoint between turnsPop out in the code editor (opens in a new tab)JavaScript
const log = []; function firstListener() {  log.push("first listener");  queueMicrotask(() => log.push("microtask"));} function secondListener() {  log.push("second listener");} firstListener();queueMicrotask(secondListener);queueMicrotask(() => console.log(log.join(", ")));

Lines 3 through 6 model the first listener and queue its small follow-up. Lines 9 through 11 name another listener. The last three lines make the two host turns visible in a controlled example. The printed list is first listener, microtask, second listener.

A real click-dispatch algorithm has more detail than this snippet. The useful rule is narrower: do not assume that every callback from one user action is one uninterrupted block. A host may reach a checkpoint between listener callbacks.

Real-life analogyCheckout queues

Imagine a supermarket checkout with a main queue and an express lane. After serving one person from the main queue, the cashier serves everyone in the express lane, including people who join while it is being served, before taking the next main-queue person.

In real life: One main checkout line
In JavaScript: Tasks
In real life: An express lane
In JavaScript: Microtasks
In real life: Cashier serves all express customers
In JavaScript: A checkpoint drains the queue
In real life: New express customer joins the lane
In JavaScript: A microtask queues another

Where the analogy stops: A supermarket has people and several cashiers; JavaScript hosts have more rules than this one-lane teaching model.

Model one checkpoint

The next source is a teaching model, not the browser's engine code. It makes the important rule visible: runTask takes one task, then repeatedly shifts microtasks until no work remains.

Tiny tasks and microtasks modelJavaScript
const tasks = ["timer"];const microtasks = ["promise"]; function runTask(label) {  const output = [label];  while (microtasks.length) output.push(microtasks.shift());  return output;} console.log(runTask(tasks.shift()).join(", "));

Lines 1 and 2 create the two queues. Line 4 starts one task. Line 6 drains the microtasks array until it is empty. Line 10 prints the modeled result timer, promise.

Step through a checkpoint model
Step 0 of 4Ready
Your turn: follow the blue line

Replay the small event-loop model: one task runs, then every queued microtask drains.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
loop.queueMicrotask("follow-up");const output = loop.runTask();console.log(output.join(", "));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay calls the real lesson model. It queues a follow-up job, runs the timer task, and records the resulting list. It is an instrumented replay, not a debugger paused inside a browser engine.

Notice the deliberate limitation: the model does not render pixels or choose among many task sources. It exists only to make the drain rule easy to test with named arrays.

Microtask starvation

Microtask starvation happens when each microtask keeps adding more microtasks, so the checkpoint never empties. The host cannot move to the timer queue or a rendering opportunity until that draining work stops.

A safe, bounded starvation examplePop out in the code editor (opens in a new tab)JavaScript
let rounds = 0;function addMicrotask() {  console.log("microtask", rounds);  rounds += 1;  if (rounds < 5) queueMicrotask(addMicrotask);}setTimeout(() => console.log("timer"), 0);queueMicrotask(addMicrotask);

Line 1 counts rounds. Lines 2 through 6 log one microtask and queue another only while the count is below five. Line 7 requests a timer. Line 8 starts the first microtask. The real output prints microtasks 0 through 4 before timer.

The limit is essential. An unbounded version would freeze the lesson sandbox and make the page unresponsive. The safe version proves the ordering without asking a reader to recover from an infinite loop.

Starvation is not a promise bug. It is a scheduling choice. A long sequence of promise reactions, mutations, or manually queued microtasks can keep doing useful work while still preventing input, timers, and painting from getting a turn.

The checkout analogy has one sharp consequence here. The cashier cannot return to the main line while people in the express lane keep adding another person to that same lane. A cap changes the story: once the fifth person is served, the express lane finally empties and the timer can receive a turn.

Yield a longer sequence through later tasksPop out in the code editor (opens in a new tab)JavaScript
let rounds = 0; function continueLater() {  console.log("round", rounds);  rounds += 1;  if (rounds < 3) setTimeout(continueLater, 0);} continueLater();

Line 1 starts a small counter. Line 5 chooses a timer for the next round instead of immediately adding a microtask. Line 9 starts the first round. The output is round 0, round 1, then round 2, with a fresh host turn between rounds.

Try the bounded queue

Change one input: how many children each modeled microtask adds. The cap keeps the output readable. Compare zero children with one and two, then watch where the timer appears.

Playground: bounded microtask growth
A checkpoint modelJavaScript
const tasks = ["timer"];
const microtasks = ["promise"];

function runTask(label) {
  const output = [label];
  while (microtasks.length) output.push(microtasks.shift());
  return output;
}

console.log(runTask(tasks.shift()).join(", "));
Modeled output

Choose a child count, then run the bounded model.

Try it yourself

Each completed microtask schedules 1 new microtask, up to a visible cap. The timer is shown only after the modeled checkpoint drains.

This is a bounded teaching model. It deliberately stops at twelve microtasks, unlike a real infinite recursion.

With zero children, the checkpoint drains immediately. With one child, it makes a chain. With two children, the queue grows faster. In every bounded run, the timer appears only after the displayed microtasks.

Reset restores one child and removes the old output. Each experiment therefore begins with the same model state rather than using leftovers from a previous queue.

queueMicrotask and promise jobs

queueMicrotask explicitly adds a callback to the same FIFO microtask queue used by ready promise jobs. The two APIs differ in how they package work, not in which queue wins over a later task.

Mix both ways to queue microtasksPop out in the code editor (opens in a new tab)JavaScript
queueMicrotask(() => console.log("microtask"));Promise.resolve().then(() => console.log("promise job"));queueMicrotask(() => console.log("last microtask"));console.log("script");

Line 1 queues the first microtask. Line 2 queues a promise job behind it. Line 3 queues one more callback. Line 4 logs synchronously. The real output is script, microtask, promise job, then last microtask.

Step through one FIFO microtask queue
Step 0 of 4Ready
Your turn: follow the blue line

Replay the FIFO order shared by queueMicrotask and promise jobs.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
loop.queueMicrotask("first");loop.queueMicrotask("second");const output = loop.runTask();console.log(output.join(", "));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

There is one practical error difference. A throw inside a then handler turns into a rejection for the promise returned by then. A throw inside queueMicrotask is reported as an uncaught error because no wrapper promise exists.

Choose queueMicrotask when you mean “run this small callback after the current stack.” Choose promises when the work naturally has a value or failure path to chain. Neither API is a way to avoid owning errors.

A promise handler has a rejection pathPop out in the code editor (opens in a new tab)JavaScript
Promise.resolve()  .then(() => {    throw new Error("promise problem");  })  .catch((error) => console.log("caught", error.message)); queueMicrotask(() => console.log("microtask still runs"));

Lines 1 through 5 create a promise chain. The throw on line 3 becomes a rejection, so line 5 logs caught promise problem. Line 7 independently queues another callback, which logs microtask still runs. A throw directly inside that callback would be reported as uncaught because there is no returned promise to catch it.

That difference matters when choosing an API. Use a promise chain when callers need a value and a rejection route. Use queueMicrotask for a small boundary callback, while still handling errors inside it or letting an uncaught error report honestly.

Using the queue in an app

Microtasks are useful for small consistency work after synchronous code finishes. They are not a general background-work system. A cart update can batch a quick recalculation, but a large loop should yield so the browser can respond.

Finish a click before a tiny updatePop out in the code editor (opens in a new tab)JavaScript
function updateCart() {  queueMicrotask(() => console.log("update total"));}updateCart();console.log("click finished");

Line 1 names an update boundary. Line 2 queues the quick total update. Line 4 calls the function. Line 5 finishes the click handler first. The output is click finished, then update total.

If the queued work keeps adding itself, use a task boundary such as a timer or another scheduler appropriate to the app. That gives the host a chance to paint, handle input, and choose other ready work.

A good boundary is small and deliberate. Queue one recalculation after a click, then let it finish. Do not turn a microtask into a hidden work loop: a checkout page should stay responsive even when several cart changes happen quickly.

Task, microtask, or host decision?
  • A zero-delay timer callback
  • A ready Promise.then handler
  • A queueMicrotask callback
  • The first script turn
  • Choosing when to drain after a task
  • Deciding whether to render before another task
Try it yourself
0 of 6 correct

Sort each card by the part of scheduling it describes.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “A microtask is a faster timer.” It has a different checkpoint rule.
  • “A checkpoint runs one callback.” It drains until the queue is empty.
  • “The specification owns the browser queue.” It uses a host hook for promise jobs.
  • “queueMicrotask makes errors into rejections.” It has no promise wrapper.
  • “Starvation means a timer was lost.” The timer waits for an endless checkpoint.
Scheduling words separated
WordWhat it meansDo not confuse it with
MicrotaskQueued work that runs at a checkpointA tiny timer or a faster timer
CheckpointDrain until the microtask queue is emptyRun exactly one promise callback
HostEnqueuePromiseJobSpecification hook from promise rules to the hostA browser API you call directly
queueMicrotask errorAn uncaught exception routeA rejected promise created by a wrapper

When output surprises you, write the synchronous logs first. Then list microtasks in creation order, including new ones created by a running microtask. Only after that should you ask which later task can run.

This small trace also stops a vague explanation like “the event loop did it.” A useful explanation names a task, a queued microtask, the checkpoint, and the next task that had to wait.

Practice exercises

Exercise 1 · Warm-upPredict the first line

Type the first printed word from the timer and promise example.

Starter codePop out in the code editor (opens in a new tab)JavaScript
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => console.log("promise"));
console.log("script");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upName the second line

    Type the second printed word.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    setTimeout(() => console.log("timer"), 0);
    Promise.resolve().then(() => console.log("promise"));
    console.log("script");

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeExplain the drain

      After one task completes, what does a microtask checkpoint drain?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeKeep the sample safe

        What kind of work should the bounded example stop after five rounds?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        let rounds = 0;
        function addMicrotask() {
          console.log("microtask", rounds);
          rounds += 1;
          if (rounds < 5) queueMicrotask(addMicrotask);
        }
        setTimeout(() => console.log("timer"), 0);
        queueMicrotask(addMicrotask);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeRead FIFO order

          Which callback runs first in the mixed example?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          queueMicrotask(() => console.log("microtask"));
          Promise.resolve().then(() => console.log("promise job"));
          queueMicrotask(() => console.log("last microtask"));
          console.log("script");

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply this to a busy page

            A product page keeps queueing microtasks while the user scrolls. What can it use to yield?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              Read each code question in the same order: current task first, then FIFO microtasks, then a later task.

              Microtask queue quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What prints first?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                Promise.resolve().then(() => console.log("promise"));
                console.log("script");

                Choose an answer to see the explanation.

              2. Question 2 of 8What does HostEnqueuePromiseJob describe?

                Choose an answer to see the explanation.

              3. Question 3 of 8At a checkpoint, what happens to a microtask queued by another microtask?

                Choose an answer to see the explanation.

              4. Question 4 of 8Why can recursive microtasks starve a timer?

                Choose an answer to see the explanation.

              5. Question 5 of 8What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                queueMicrotask(() => console.log("a"));
                Promise.resolve().then(() => console.log("b"));
                console.log("script");

                Choose an answer to see the explanation.

              6. Question 6 of 8How does an exception in queueMicrotask differ from one in a promise handler?

                Choose an answer to see the explanation.

              7. Question 7 of 8Which statement keeps the specification and the host separate?

                Choose an answer to see the explanation.

              8. Question 8 of 8What does this bounded model print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const jobs = ["promise"];
                const output = ["task"];
                while (jobs.length) output.push(jobs.shift());
                console.log(output.join(", "));

                Choose an answer to see the explanation.

              Key takeaways

              • ECMAScript defines promise jobs as later ordered work, while hosts own their queues.
              • HostEnqueuePromiseJob hands ready promise work to a browser or Node host.
              • A checkpoint drains microtasks after a task and before a later task.
              • Recursive microtasks can starve timers and rendering until the queue becomes empty.
              • Promise jobs and queueMicrotask callbacks share FIFO order, but have different error routes.

              Remember the one-liner.
              A host runs one task, then drains every ready microtask before choosing the next task.

              Coming next: How async code is compiled down.

              CompleteFrontend Clear concepts. Working examples.