completefrontend
Open lab My progress
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
    Tell the two layers apartSay which promise rules come from ECMAScript, the JavaScript rulebook, and which queue and checkpoint choices belong to the browser or Node.
  • 02
    Follow a checkpointPredict the output of tasks and microtasks: after one task finishes, every waiting microtask runs before the next task.
  • 03
    Avoid starvationSpot microtasks that keep adding microtasks, and know when to use a later task instead, so the page gets a turn.

A small queue with a big effect

A callback is a function you hand over to be called later. A browser or Node does not run every waiting callback at once. First, the code that is running now finishes. Then the browser or Node picks what runs next.

In this lesson, the browser or Node is called the host: the program that runs your JavaScript. The host runs work in pieces:

  • A task is one piece of work the host runs from start to end. The whole script is a task. So is one timer callback, or one click handler.
  • A queue is a waiting line. The first one in is the first one out.
  • Promise callbacks wait in a special line called the microtask queue.
Definition

A microtask is a small piece of work that waits in the microtask queue. The host runs it at a microtask checkpoint: a moment when the host stops and runs every waiting microtask. A common checkpoint is right after the current code finishes, before the host picks its next task. Each host decides its exact checkpoints.

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 callback later. setTimeout(callback, 0) means "run this as a new task, as soon as you can". Line 2 makes a promise that is already done and gives it a then callback. Because the promise is ready, the callback goes into the microtask queue. Line 3 logs right away. The output is script, then promise, then timer.

Three cards in a row: script, then promise, then timer. Script runs now in this task. Promise runs after the task. Timer is the next task and waits its turn.scriptpromisetimer1. this taskruns now2. microtaskafter the task3. next taskwaits its turn
The script finishes first. Then the promise callback runs from the microtask queue. The timer is a new task, so it runs last.

The Microtasks lesson showed this order. This lesson looks closer. Where do promise jobs wait? And why does the host run all of them before a timer, a click, or any other task?

Promise jobs in the language rules

ECMAScript is the official rulebook for JavaScript. People also call it the specification, or the spec. The spec calls work that must happen later a job. When a promise is done and has a then callback, the spec makes a promise job to call it.

The spec says promise jobs wait and run later, in order. It does not describe browser tabs, Node, the screen, or timers. Those belong to the host.

A ready promise job waits for the current codePop 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 makes a promise job for the then callback. Line 2 logs while the current task is still running. Line 3 shows that the task keeps going. The output is task, task still running, then job.

This split is useful. The spec can promise that a then callback is never called in the middle of your current code. At the same time, browsers and Node are free to fit jobs into their own way of running tasks. How they store the queue inside is up to them.

So when you debug, think in two layers:

  • The language says: a ready promise callback runs later.
  • The host says when the current task ends and when a checkpoint starts.
Run it: the job prints lastPop 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");

Run the same three lines yourself. Line 1 adds the callback to a promise that is already done. Lines 2 and 3 are ordinary code in the current task. The output is again task, task still running, then job.

The word job is broad on purpose. It means a piece of later work in the language rules. It is not a queue you can see in the browser. If a browser changes how it draws the page, or Node changes how it reads files, the promise rule stays the same.

HostEnqueuePromiseJob: the hand-over

HostEnqueuePromiseJob is a step in the spec. "Enqueue" means "add to a queue". The promise rules use this step to hand a ready promise job to the host.

You can't call it from your code. It marks the place where the promise rules end and the browser's or Node's queue begins.

A promise callback is handed to the hostPop 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 makes a promise that is already done. Line 2 adds a callback. The promise rules then use HostEnqueuePromiseJob to hand a job for that callback to the host. Line 3 runs before that job. The output is take next order, then serve tea.

The name helps you avoid a common mistake. ECMAScript sets the promise rules. But no line of your code reaches inside Chrome or Node and pushes onto their queue. Your code asks for promise behavior. The host receives the job.

This is why the docs for different hosts can sound different, while the basic promise order stays the same. The shared rule is: promise callbacks run later. The tasks around them, and when the page is drawn, depend on the host.

A tiny pretend version of the hand-overJavaScript
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. It stands in for the host's microtask queue. Lines 3 to 5 are a pretend version of the step: it takes a job and stores it. Line 7 hands over the tea job. Line 8 still prints first. Line 9 takes the stored job out and calls it. shift() removes the first item of an array and returns it.

Left: a box called promises holds the serve tea job. An arrow named HostEnqueuePromiseJob hands the job over. Right: a box called host queue now holds the serve tea job.serve teapromisesJavaScript rulesHostEnqueuePromiseJobhands over the jobserve teahost queuebrowser or Node
The promise rules make the job. HostEnqueuePromiseJob hands it to the browser or Node, which keeps it in its own queue until it is time to run it.

This is not the browser's real code. It only shows the hand-over. The promise rules ask for a job to be queued. They don't say how the host stores the queue, or when it picks its next task.

Microtask checkpoints

A microtask checkpoint is the moment the host runs the microtask queue until it is empty. People call this draining the queue. A simple way to picture one turn of the host:

  1. Run one task.
  2. Wait until the JavaScript stack is empty. The stack is the list of functions running right now. Empty means no JavaScript is running.
  3. Run every microtask, one by one, until none are left.
  4. Only then, pick the next task.

Hosts have other checkpoints too, for example between two click listeners. You'll see that later in this section.

A microtask can add 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 to 4 queue a callback. That callback, on line 3, queues one more callback. Line 5 logs right away. At the checkpoint, the first callback runs and adds the second one to the queue. The checkpoint keeps going, so the second one runs too. The output is script, first, then second.

A script card, then a cart called queue holding the microtasks first and second. A curved arrow shows first adds second. Both run before a later task card.script1. task endsaddsfirstsecondqueue2. drain until emptylater task3. waits
While first runs, it adds second to the same queue. The checkpoint keeps going until the queue is empty, so second also runs before any later task.

Browsers also run checkpoints around events, and before they draw the page. For example, when a user clicks, there can be a checkpoint between two click listeners. When the page gets drawn is a browser rule, not an extra promise rule.

A pretend click with a checkpoint between listenersPop 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 to 6 stand in for the first listener. It logs, then queues a small follow-up microtask. Lines 8 to 10 stand in for a second listener. Line 12 runs the first listener. Lines 13 and 14 queue the second listener and a final log, behind the follow-up. The printed list is first listener, microtask, second listener.

A real click has more steps than this. The point is smaller: don't assume every callback from one click runs as one unbroken block. The host may run a checkpoint between two listeners.

Real-life analogyCheckout lines

Picture a shop with one cashier and two lines: a main line and an express lane. After serving one person from the main line, the cashier serves everyone in the express lane. That includes people who join the express lane while the cashier is serving it. Only when the express lane is empty does the cashier take the next person from the main line.

In real life: The main checkout line
In JavaScript: Tasks
In real life: The express lane
In JavaScript: Microtasks
In real life: The cashier serves the whole express lane
In JavaScript: A checkpoint drains the queue
In real life: A new person joins the express lane
In JavaScript: A microtask queues another

A small model of one checkpoint

The next code is a teaching model. It is not the browser's real code. It shows one rule clearly: runTask takes one task, then takes microtasks out one by one until none are left.

Two arrays: tasks and microtasksJavaScript
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 make the two queues as arrays. Lines 4 to 8 define runTask, which runs one task. Line 6 is the drain: while the microtasks array still has items, it moves the first one into the output. Line 10 runs the timer task and prints timer, promise.

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

Step through the small model: one task runs, then every waiting microtask runs.

Running in
  1. script
Next: line 1
loop.queueMicrotask("follow-up");const output = loop.runTask();console.log(output.join(", "));
CallStoreChangeResultBlue line: runs next. 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 step-through calls the lesson's real model. It queues a follow-up job, runs the timer task, and shows the result list. It replays the model code. It is not a debugger paused inside a browser.

The model leaves things out on purpose. It does not draw the page. It does not choose between many kinds of tasks. It only makes the drain rule easy to see and test.

Microtask starvation

Microtask starvation happens when each microtask adds another microtask, so the checkpoint never ends. The host can't get to the timer, or draw the page, until the microtasks stop. The waiting work "starves": it never gets a turn.

A safe starvation example that stops after five roundsPop 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 to 6 define addMicrotask. It logs one round, adds 1 to the count, and queues itself again while the count is below 5. Line 7 asks for a timer. Line 8 starts the first microtask. The output is microtask 0 to microtask 4, then timer.

Five microtask cards, 0 to 4, joined by arrows. Each one adds the next. After card 4 the chain stops, and only then does the timer card run.microtasks 0 to 401234each one adds the nexttimerruns only afterthe chain stops
Each microtask adds the next one, so the queue is never empty until rounds reaches 5. Only then can the timer run.

The limit of 5 matters. A version without a limit would never stop. It would freeze the page, and you would have to close the tab.

Starvation is not a promise bug. It comes from how the work is scheduled. A long chain of promise callbacks or queued microtasks can do useful work, and still keep clicks, timers, and drawing from getting a turn.

Back to the checkout lines: the cashier can't go back to the main line while each person in the express lane calls one more friend into it. With the limit, the fifth person brings no one. The express lane empties, and the timer gets its turn.

Spread the work over several 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();

Yielding means pausing your work so the host can run other things first. Line 1 starts a counter. Lines 3 to 7 define continueLater. Line 6 asks for a timer for the next round, instead of adding a microtask. Line 9 starts the first round. The output is round 0, round 1, then round 2. Between rounds, the host gets a fresh turn.

Try it: how fast the queue grows

Change one setting: how many new microtasks each microtask adds. The playground calls these its "children". The model stops at 12 microtasks, so the output stays short. Try 0, then 1, then 2, and watch where the timer appears.

Playground: how fast the microtask queue grows
The 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(", "));
Order the model prints

Pick a number, then press Run.

Try it yourself

Each microtask adds 1 new microtask, until the model reaches 12. The timer comes only after every microtask has run.

A teaching model. It stops at 12 microtasks on purpose, so it never becomes an endless loop.

With 0 children, the queue is empty after the first microtask. With 1 child, each microtask adds the next one, like a chain. With 2 children, the queue grows faster. In every run, the timer comes only after all the microtasks shown.

Reset goes back to 1 child and clears the old output. So every try starts from the same state.

queueMicrotask and promise jobs share one queue

queueMicrotask(callback) adds a callback straight to the microtask queue. Ready promise jobs wait in that same queue. The queue is FIFO: first in, first out. So callbacks run in the order they were added, whichever way they were added.

Both ways to queue a microtask, mixedPop 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 right away. The output is script, microtask, promise job, then last microtask.

Step through first-in, first-out order
Step 0 of 4Ready
Your turn: follow the blue line

Step through the first-in, first-out order that queueMicrotask and promise jobs share.

Running in
  1. script
Next: line 1
loop.queueMicrotask("first");loop.queueMicrotask("second");const output = loop.runTask();console.log(output.join(", "));
CallStoreChangeResultBlue line: runs next. 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 real difference between the two. It shows when the callback throws an error:

  • A throw inside a then callback makes the promise returned by then fail. A .catch later in the chain can handle it.
  • A throw inside a queueMicrotask callback is reported as an uncaught error. There is no promise around it to catch it.

Use queueMicrotask when you mean "run this small callback right after the current code". Use promises when the work has a result or an error to pass along. With either one, you still have to handle errors.

A promise callback has a path for errorsPop 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 to 5 make a promise chain. The throw on line 3 makes the chain fail, so the .catch on line 5 logs caught promise problem. Line 7 queues a separate callback, which logs microtask still runs.

The output is microtask still runs first, then caught promise problem. The .catch callback is only queued after the throw on line 3 happens. By then, the callback from line 7 is already waiting in the queue.

If the callback on line 7 threw an error, nothing would catch it. There is no promise for it to fail.

So pick by what you need. Use a promise chain when the caller needs a result and a way to handle errors. Use queueMicrotask for a small callback right after the current code, and handle errors inside it.

Using the queue in an app

Microtasks are good for small tidy-up work right after the current code finishes. They are not for heavy work. For example, after a cart changes, you can queue one quick update of the total. But a big loop should pause, so the browser can respond to the user.

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

Lines 1 to 3 define updateCart. 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 more of itself, use a task instead, such as a timer. That lets the host draw the page, handle clicks, and run other waiting work.

Keep each microtask small and on purpose. Queue one update after a click, and let it finish. Don't let a microtask quietly turn into a loop that never ends. A checkout page should stay quick, even when the cart changes many times in a row.

Task, microtask, or host decision?
  • A timer callback with a delay of 0
  • A then callback on a promise that is done
  • A queueMicrotask callback
  • Running the whole script the first time
  • Choosing when to run the microtasks after a task
  • Deciding whether to draw the page before the next task
Try it yourself
0 of 6 correct

Sort each card: is it a task, a microtask, or something the host decides?

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

Common misconceptions

  • "A microtask is just a faster timer." It is not a timer at all. It runs at a checkpoint, before the next task.
  • "A checkpoint runs one callback." It keeps running callbacks until the queue is empty.
  • "The spec controls the browser's queue." The spec only hands promise jobs to the host, through HostEnqueuePromiseJob.
  • "queueMicrotask turns errors into failed promises." There is no promise around its callback, so an error is reported as uncaught.
  • "Starvation means a timer got lost." The timer is still waiting. It runs once the checkpoint ends.

Each row of this table is one word from the lesson: what it means, and what it is easy to mix it up with.

Words from this lesson that are easy to mix up
WordWhat it meansWhat it is not
MicrotaskSmall work that waits in a queue and runs at a checkpoint.A tiny timer, or a faster timer.
CheckpointRunning microtasks until the queue is empty.Running exactly one promise callback.
HostEnqueuePromiseJobThe spec step that hands a promise job to the host.A browser function you can call yourself.
Error in queueMicrotaskIt is reported as an uncaught error.A failed promise that .catch can handle.

When an output surprises you, write it out in this order:

  1. The normal logs from the current code.
  2. The microtasks, in the order they were queued. Include new ones that a running microtask adds.
  3. Only then, the next task, such as a timer.

This also stops vague answers like "the event loop did it". The event loop is the host's cycle: pick a task, run it, run a checkpoint, repeat. A useful answer names the task, the queued microtask, the checkpoint, and the task that had to wait.

Practice exercises

Exercise 1 · Warm-upPredict the first line

Type the first word this timer and promise example prints.

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 word it prints.

    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 finishes, what does a microtask checkpoint run until it is empty?

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

        Exercise 4 · PracticeKeep the example safe

        In the safe example, what kind of work stops 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 first-in, first-out 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 pause, so the page can respond?

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

              Check your understanding

              Read each code question in the same order: the current code first, then the microtasks in the order they were queued, then any 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 do?

                Choose an answer to see the explanation.

              3. Question 3 of 8At a checkpoint, what happens to a microtask that another microtask added?

                Choose an answer to see the explanation.

              4. Question 4 of 8Why can microtasks that keep adding 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 is an error thrown in a queueMicrotask callback different from one thrown in a promise callback?

                Choose an answer to see the explanation.

              7. Question 7 of 8Which sentence keeps the spec and the host apart?

                Choose an answer to see the explanation.

              8. Question 8 of 8What does this small 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 says promise jobs run later, in order. The host (a browser or Node) owns the real queues.
              • HostEnqueuePromiseJob is the spec step that hands a ready promise job to the host.
              • A checkpoint runs every waiting microtask after a task, before the next task.
              • Microtasks that keep adding microtasks stop timers and page drawing from getting a turn, until the queue is empty.
              • Promise jobs and queueMicrotask callbacks share one first-in, first-out queue, but handle errors differently.

              Remember the one-liner.
              A host runs one task, then runs every waiting microtask, then picks the next task.

              Coming next: How async code is compiled down.

              CompleteFrontend Clear concepts. Working examples.