completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

Microtasks in depth

Predict the output of promise jobs, queueMicrotask callbacks, async await continuations, timers, and nested microtasks with real browser puzzle runs.

By the end you can
  • 01
    Spot microtasks in codeFind the three things that add microtasks: .then callbacks, queueMicrotask, and code after await.
  • 02
    Predict the printed orderOrder ordinary code, microtasks, microtasks added by microtasks, and timer tasks.
  • 03
    Use microtasks safelyUse a microtask for a small follow-up job, without making the next task wait forever.

Microtasks: small jobs that wait for your code to finish

In the last lesson you met the event loop. Here is a short reminder:

  • A task is one piece of work, like running your script or running a timer callback.
  • JavaScript runs one task at a time, from start to end.
  • The call stack is the list of functions that are running right now. When a task ends, the call stack is empty.

This lesson is about the short gap between one task and the next.

A microtask is a small job that waits for the current code to finish. It runs after the current task, but before the next task, such as a timer callback. Three things make microtasks:

  • the callback you pass to .then on a promise,
  • the callback you pass to queueMicrotask,
  • the rest of an async function after await.
Real-life analogySticky notes before the next meeting

Your meeting ends. Before you walk to the next meeting, you deal with every sticky note on your desk. If a note makes you write a new note, you deal with that one too. You leave only when the desk is clear.

In real life: Your current meeting
In JavaScript: The current task, like your script
In real life: Sticky notes on your desk
In JavaScript: Microtasks waiting to run
In real life: A note that says: call Ada
In JavaScript: A promise job: call this .then callback
In real life: Walking to the next meeting
In JavaScript: Starting the next task, like a timer callback
The main rule

When the current task ends and the call stack is empty, JavaScript runs every waiting microtask, in the order they were added. If a microtask adds a new microtask, that one runs too. The next task can start only when no microtasks are left. This moment between tasks is called the microtask checkpoint.

This lesson uses only a few facts about promises and await. The next lessons cover them in full. For now you need two facts:

  • When a promise is settled (finished), its .then callback is added as a microtask.
  • await x pauses an async function. The rest of the function runs later, as a microtask.

Promise jobs: a .then callback waits its turn

PROMISE JOBS

A promise is an object that will hold a result. A promise job is a microtask that calls a .then callback with that result. In the code below, Promise.resolve() makes a promise that already has its result. Even so, the .then callback does not run right away. JavaScript adds a job to the microtask queue that says: "call this callback with the result."

A promise job waits for the other linesPop out in the code editor (opens in a new tab)JavaScript
console.log("sync A");Promise.resolve().then(() => console.log("promise job"));console.log("sync B");// sync A, sync B, promise job

In plain words, line by line:

  1. Line 1 prints sync A right away.
  2. Line 2 adds the .then callback to the microtask queue. Nothing prints yet.
  3. Line 3 prints sync B right away.
  4. The script ends. Now the microtask runs and prints promise job.
The printed order. Card 1, sync A, from line 1, runs now. Card 2, sync B, from line 3, runs now. Card 3, promise job, from line 2, is a microtask and runs later.ordinary codesync Async Ba microtaskpromise jobline 1: nowline 3: nowline 2: later
Lines 1 and 3 print right away. The .then callback on line 2 waits as a microtask, so it prints last.
Two .then callbacks keep their orderPop out in the code editor (opens in a new tab)JavaScript
console.log("start");Promise.resolve().then(() => console.log("first reaction"));Promise.resolve().then(() => console.log("second reaction"));console.log("end");// start, end, first reaction, second reaction

Lines 2 and 3 each add one .then callback to the microtask queue. Lines 1 and 4 are ordinary code, so they print first: start, then end. Then the two callbacks run in the order they were added. So a .then callback runs after the ordinary lines, but before a timer callback.

Real-life analogyA promise job is a note to call someone back

A finished promise does not stop the line that is running. It leaves a note instead: when the current work is done, call this callback with the result.

In real life: A note says: call Sam with the answer
In JavaScript: A promise job says: call this .then callback with the result
In real life: You finish your sentence first
In JavaScript: The current code runs to the end
In real life: Then you call Sam
In JavaScript: The promise job runs at the microtask checkpoint

queueMicrotask: add your own microtask

INTERACTIVE

queueMicrotask(fn) adds the function fn to the microtask queue. You call it directly, without a promise. Use it when you want a small follow-up job to run:

  • after the current code finishes,
  • but before timers, clicks, or the next screen update get a turn.

The table below lists the common ways to make a microtask, plus one way that is not a microtask. Read each row as: "when I write this, this gets added, and this is what to remember."

Common ways to add a microtask
When you writeWhat gets addedWhat to remember
Promise.resolve().then(fn)A promise job that calls fn.fn runs after the current code finishes.
queueMicrotask(fn)fn itself, as a microtask.You add it directly. If fn throws, the error is uncaught.
await valueThe rest of the async function.The function pauses, then continues in a later microtask.
A microtask that adds another microtaskA new microtask at the back of the same queue.It still runs before the next task.
setTimeout(fn, 0)A task, not a microtask.It waits until the microtask queue is empty.

Promise jobs and queueMicrotask callbacks wait in the same queue. This queue is FIFO, which means "first in, first out", like a line at a shop. Whatever was added first runs first.

A microtask can also add another microtask. The new one goes to the back of the same queue, and it still runs before the next task. The experiment below shows this with a timer that is already waiting.

A microtask added by a microtask still beats a waiting timer
A microtask adds another microtaskPop out in the code editor (opens in a new tab)JavaScript
console.log("task start");setTimeout(() => console.log("timeout"), 0);queueMicrotask(() => {  console.log("micro 1");  queueMicrotask(() => console.log("micro 2"));});Promise.resolve().then(() => console.log("promise"));console.log("task end");
Printed order
    Try it yourself

    Run the code and watch for micro 2. If JavaScript ran only the microtasks that were waiting at the start, timeout would print before micro 2.

    This runs real queueMicrotask, Promise.resolve().then, and setTimeout calls in your browser.
    The microtask queue holds three cards in order: micro 1, promise, micro 2. An arrow from micro 1 to micro 2 says micro 1 adds micro 2. Below, the timeout card is the next task and runs 4th, after the queue is empty.micro 1 adds micro 2micro 1promisemicro 21st2nd3rdnext tasktimeout4th: when the queue is empty
    micro 1 adds micro 2 to the back of the same queue. The timer runs only when the microtask queue is empty.

    Next, see what happens when a .then callback throws an error.

    An error in .then rejects the promise
    An error inside a .then callbackPop out in the code editor (opens in a new tab)JavaScript
    Promise.resolve()  .then(() => {    console.log("then A");    throw new Error("boom");  })  .catch(() => console.log("caught rejection"));queueMicrotask(() => console.log("queued microtask"));console.log("sync");
    Printed order
      Try it yourself

      Click to run the promise code. An error inside a queueMicrotask callback would be an uncaught error, so this lesson only describes that case instead of running it.

      Only the promise code runs here. The queueMicrotask error case is described, not run.

      Some browser callbacks look alike but run at different times. A MutationObserver watches part of the page, called the DOM, and calls you back when it changes. Read each row of the table as: "this callback waits here, and this is when it runs."

      Browser callbacks that are easy to mix up
      CallbackWhere it waitsWhen it runs
      MutationObserverThe microtask queue.It runs after the page changes, before the next task.
      setTimeoutThe task queue.It runs later, after the microtask checkpoint.
      requestAnimationFrameA list of jobs for the next screen update.It runs just before the browser draws the page. It is not a microtask.
      MutationObserver reports a change at the microtask checkpointJavaScript
      const box = document.createElement("div");const observer = new MutationObserver(() => console.log("mutation"));observer.observe(box, { childList: true });box.append("done");console.log("sync");// sync, mutation

      In plain words, line by line:

      1. Line 1 makes an empty div called box.
      2. Line 2 makes an observer whose callback prints mutation.
      3. Line 3 tells the observer to watch box for new children.
      4. Line 4 adds the text done to the box. The callback does not run yet.
      5. Line 5 prints sync.

      The browser reports the change at the microtask checkpoint, so mutation prints after sync. This is one reason microtasks matter outside promise code.

      Errors work differently

      If a .then callback throws an error, its promise becomes rejected (failed). A later .catch can then handle the error. If a queueMicrotask callback throws, there is no promise, so no .catch can handle it. The browser reports it as an uncaught error. The experiment above runs only the promise side, so it doesn't break this page.

      await: pause now, continue later

      AWAIT

      An async function is a function written with the async keyword. Inside it you can use await. When you call one, this happens:

      1. The function starts right away, like any other function.
      2. When it reaches await x, it pauses.
      3. The code that called it keeps running.
      4. Later, the rest of the function runs as a microtask.

      JavaScript engines have changed how many inner steps await takes, so don't try to count them. Predict the order from real runs, like the ones in this lesson.

      Real-life analogyawait is a bookmark

      You read a book up to your bookmark. The doorbell rings, so you close the book and answer the door. Then you come back and keep reading from the bookmark. await does the same for an async function.

      In real life: Read until the bookmark
      In JavaScript: Run the async function until await
      In real life: Close the book for a moment
      In JavaScript: Let the rest of the script run
      In real life: Open the book at the bookmark
      In JavaScript: Continue the async function in a microtask
      Code after await runs laterPop out in the code editor (opens in a new tab)JavaScript
      async function demo() {  console.log("before await");  await null;  console.log("after await");}demo();console.log("outside");// before await, outside, after await

      In plain words, in the order things happen:

      1. Line 6 calls demo().
      2. Line 2 prints before await right away.
      3. Line 3 pauses demo.
      4. Line 7 prints outside.
      5. The script ends. The rest of demo runs as a microtask: line 4 prints after await.
      The printed order. Card 1, before await: demo starts and runs now. Then await null pauses it. Card 2, outside: the script goes on. Card 3, after await: the rest of demo runs later as a microtask.before awaitawait nulloutsideafter await1. runs now2. script goes on3. a microtask
      demo() runs until await null and pauses. The script prints outside, then the rest of demo runs as a microtask.
      await works with a plain numberPop out in the code editor (opens in a new tab)JavaScript
      async function readValue() {  console.log("before");  const value = await 3;  console.log("value", value);}readValue();console.log("outside");// before, outside, value 3

      await does not need a promise. Line 3 waits for the number 3. JavaScript treats 3 like a promise that already has its result. The rest of the function still waits for a microtask. So before and outside print first, then value 3.

      The puzzle gallery below has three await puzzles:

      • a basic pause,
      • two async functions that take turns,
      • a thenable: a plain object that has its own then method.

      Awaiting a thenable adds an extra step. JavaScript first calls the object's then method in a microtask. For that puzzle, this lesson states only the order that the real run shows.

      Step through one puzzle, queue by queue

      STEP THROUGH

      The player below steps through the second puzzle, the one that prints A to F. It shows what has printed, what waits in the microtask queue, and what waits as the next task. It is a teaching model, not a real debugger, but its final order comes from a real run. Each step shows one of these events:

      • a promise job is added,
      • a microtask is added with queueMicrotask,
      • a timer task is added,
      • a microtask runs,
      • the timer task runs.
      Step through the A to F puzzle, queue by queue
      Step 0 of 10Ready
      Your turn: follow the blue line

      This is a teaching model of the queues for the A to F puzzle. Its order comes from a real run, but it is not a real debugger.

      Running in
      1. script
      Next: line 1
      Promise.resolve().then(() => {  console.log("B");  Promise.resolve().then(() => console.log("D"));});queueMicrotask(() => console.log("C"));setTimeout(() => console.log("E"), 0);console.log("F");
      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.

      Where you'll use microtasks

      You won't often call queueMicrotask yourself. But promises and async functions are everywhere: loading data, updating the page, and testing. Knowing the order helps you fix bugs like "why did this print first?"

      Collect calls, then print oncePop out in the code editor (opens in a new tab)JavaScript
      let queued = false;const pending = []; function scheduleMessage(message) {  pending.push(message);  if (queued) return;  queued = true;  queueMicrotask(() => {    queued = false;    console.log(pending.join(", "));    pending.length = 0;  });} scheduleMessage("first");scheduleMessage("second");// first, second

      This pattern is called batching: collect several small jobs, then handle them together. In plain words, line by line:

      1. Line 1: queued remembers whether a microtask is already waiting.
      2. Line 2: pending is a list that collects the messages.
      3. Line 5 adds the message to the list.
      4. Line 6: if a microtask is already waiting, the function stops here.
      5. Lines 7 and 8: otherwise, it sets queued to true and adds one microtask.
      6. Lines 9 to 11 run later: they reset queued, print every message, and empty the list.
      7. Lines 15 and 16 call the function twice. Only the first call adds a microtask.

      So the output is one line: first, second.

      Two calls, first and second, both go into a box called pending. Later, one microtask prints the whole list once in the console: first, second.two callsfirstsecondfirstsecondpendingboth wait herelaterconsolefirst, secondprinted once
      Both calls add their message to pending. Later, one microtask prints the whole list in one line.

      Keep the microtask short. If the work could take a long time, split it into tasks, such as timer callbacks, so the page can still respond.

      Node.js has an extra queue that goes firstJavaScript
      process.nextTick(() => console.log("nextTick"));Promise.resolve().then(() => console.log("promise"));queueMicrotask(() => console.log("queueMicrotask"));console.log("sync");// sync, nextTick, promise, queueMicrotask

      In Node.js, process.nextTick adds a callback to a special Node queue. That queue runs before promise jobs and queueMicrotask callbacks. So this code prints sync, nextTick, promise, queueMicrotask. Don't use it in browser code: browsers have no process object and no nextTick queue.

      Common misconceptions

      "A promise that already has its result runs its .then callback right away."

      No. The callback is added as a microtask. It runs after the current code finishes.

      "setTimeout(fn, 0) means right now."

      No. It means a future task. Microtasks added by the current task run first.

      "JavaScript runs only the microtasks that were waiting at the start."

      No. It keeps going until the queue is empty. A microtask added by another microtask still runs before the next task.

      "queueMicrotask and Promise.resolve().then are the same."

      They run in the same queue, in the same order. But errors are handled differently.

      "await always takes the same number of steps."

      No. Engines have changed and sped up await. Trust real runs, not old step-counting rules.

      "You can only await a promise."

      No. You can await any value. Even after await 3, the rest of the async function runs later.

      "Every place that makes microtasks works the same way."

      No. Browsers and Node.js add their own details. For example, Node's process.nextTick runs before promise jobs.

      Practice: predict the order

      6 EXERCISES
      Exercise 1 · Warm-upOrder a promise job

      Predict the output order, then type the three words in order.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      console.log("one");
      Promise.resolve().then(() => console.log("two"));
      console.log("three");

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

        Exercise 2 · PracticeA microtask before a timer

        Predict the order. Why does the microtask print before a timer with a 0ms delay?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        setTimeout(() => console.log("timeout"), 0);
        queueMicrotask(() => console.log("micro"));
        console.log("sync");

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

          Exercise 3 · PracticeOrder an async function

          What prints before the pause, and what prints after it?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          async function label() {
            console.log("A");
            await null;
            console.log("B");
          }
          label();
          console.log("C");

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

            Exercise 4 · PracticeAdd a small job with queueMicrotask

            Read the program and type what the timer prints.

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const steps = [];
            steps.push("start");
            queueMicrotask(() => steps.push("microtask"));
            steps.push("end");
            setTimeout(() => console.log(steps.join(" -> ")), 0);

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

              Exercise 5 · ChallengeFix a microtask loop that never ends

              Explain the bug, then compare your idea with the solution. This one is not checked automatically, because the fix is a change of design: stop the loop, or give the browser a turn.

              Starter codePop out in the code editor (opens in a new tab)JavaScript
              function riskyLoop() {
                queueMicrotask(riskyLoop);
              }
              riskyLoop();
                Exercise 6 · PracticeAwait a plain value

                Predict which line prints first when an async function awaits the number 3.

                Starter codePop out in the code editor (opens in a new tab)JavaScript
                async function showPrice() {
                  const price = await 3;
                  console.log(price);
                }
                showPrice();
                console.log("outside");

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

                  Quiz: check your understanding

                  8 QUESTIONS
                  Lesson quiz · 8 questionsScore: first tries count
                  1. Question 1 of 8What is a promise job?

                    Choose an answer to see the explanation.

                  2. Question 2 of 8What does this code with a promise and queueMicrotask print?

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

                    Choose an answer to see the explanation.

                  3. Question 3 of 8Which sentence about queueMicrotask is true?

                    Choose an answer to see the explanation.

                  4. Question 4 of 8What does this async function code print?

                    Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                    async function demo() {
                      console.log("start");
                      await null;
                      console.log("after");
                    }
                    demo();
                    console.log("end");

                    Choose an answer to see the explanation.

                  5. Question 5 of 8A microtask adds another microtask while a 0ms timer is waiting. What happens?

                    Choose an answer to see the explanation.

                  6. Question 6 of 8What does this code with an error in .then print?

                    Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                    Promise.resolve()
                      .then(() => {
                        console.log("then");
                        throw new Error("boom");
                      })
                      .catch(() => console.log("catch"));
                    queueMicrotask(() => console.log("micro"));
                    console.log("sync");

                    Choose an answer to see the explanation.

                  7. Question 7 of 8Why is a chain of microtasks that never ends a problem?

                    Choose an answer to see the explanation.

                  8. Question 8 of 8What happens when an async function awaits the number 3?

                    Choose an answer to see the explanation.

                  Key takeaways

                  • A .then callback runs as a microtask, after the current code finishes.
                  • queueMicrotask adds a microtask directly. Keep that job small.
                  • await runs the code before it now, and the code after it later, as a microtask.
                  • All microtasks run before the next task, even ones added by other microtasks.
                  • Microtasks that keep adding more microtasks can block timers, clicks, and screen updates.

                  One-liner.
                  A microtask is a small job that runs after the current code finishes and before the next task starts.

                  Up next: Callbacks.

                  CompleteFrontend Clear concepts. Working examples.