cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Events, microtasks & ordering

Learn why real clicks and script-dispatched events can run listeners and microtasks in different orders in JavaScript.

By the end, you can
  • 01
    Trace a clickPredict listener and microtask order by asking when the current call stack becomes empty.
  • 02
    Separate event sourcesExplain why user input, click(), and dispatchEvent() do not create the same dispatch boundary.
  • 03
    Choose the next queueReason about promises, await, timers, and isTrusted without guessing from timing.

One click, two possible orders

A click can call the same listeners but produce a different log depending on who started it. A physical click comes from the browser. button.click() comes from JavaScript that is already running.

Definition

Event dispatch is the browser or script calling listeners for an event. A microtask is small queued work, such as a promise reaction, that runs when JavaScript reaches a microtask checkpoint.

The central puzzle has two listeners. Each listener logs once and queues one microtask. A real user click logs listener 1, microtask 1, listener 2, microtask 2. A script click logs both listeners before either microtask.

This is not a promise race. It is a call-stack question. Ask one thing first: has the current JavaScript stack become empty yet?

That question also prevents a common debugging mistake. Seeing two logs close together does not prove they ran in one kind of queue. The logs can be adjacent while one is synchronous, one is a microtask, and one is a later task. Label the boundary before you change code.

There is no promise that a product feature should use these low-level boundaries as its business workflow. The lesson explains what the platform does so that a test failure or a surprising console log has a small, concrete explanation. Your application can still choose a clearer sequence.

User input and script dispatch

Start with the script version because it is short and runnable. Lines 3 through 5 register listener 1. Lines 7 through 9 register listener 2. Line 12 calls button.click() from the same script.

Script dispatch: button.click()Pop out in the code editor (opens in a new tab)JavaScript
const button = document.querySelector("#order-button"); button.addEventListener("click", () => {  console.log("listener 1");  queueMicrotask(() => console.log("microtask 1"));}); button.addEventListener("click", () => {  console.log("listener 2");  queueMicrotask(() => console.log("microtask 2"));}); button.click();

It prints listener 1, listener 2, microtask 1, microtask 2. Line 12 does not return until both listeners have run. Only after that script call finishes can the microtask queue drain.

Now register the same listeners and click the visible button yourself. The listener bodies are identical. The important change is the browser-created input boundary around the user action.

Real user click: click the visible buttonPop out in the code editor (opens in a new tab)JavaScript
const button = document.querySelector("#order-button"); button.addEventListener("click", () => {  console.log("listener 1");  queueMicrotask(() => console.log("microtask 1"));}); button.addEventListener("click", () => {  console.log("listener 2");  queueMicrotask(() => console.log("microtask 2"));}); // Click the visible button with your mouse or keyboard.

Chrome evidence for this lesson shows listener 1, microtask 1, listener 2, microtask 2 for a real page.click(). The test also checks the script order separately. Keep those facts scoped to the event sources being compared.

Registering listener 1 before listener 2 still matters. Both versions preserve that registration order for the listener calls themselves. The difference is where each listener call ends relative to a checkpoint, not a reversal of listener registration.

Keyboard activation is also real user input. It is useful to test it separately when a control has keyboard behavior, but it is not a replacement for understanding the dispatch boundary. This lesson keeps one visible button so the only moving part is where the click came from.

Four things that look similar but are not
SourceWho starts it?Observed order
A real user clickThe browser delivers trusted input through a user-input boundary.Listener 1, microtask 1, listener 2, microtask 2 in this lesson's Chrome evidence.
button.click()Your running script synchronously dispatches an untrusted click.Listener 1, listener 2, microtask 1, microtask 2.
dispatchEvent(event)The method calls matching listeners synchronously before it returns.Anything after the call waits until listeners return.
A queued microtaskA small job that waits for the current JavaScript stack to empty.It runs before a later timer task, not in the middle of one active stack.

Microtasks between listeners

A microtask does not run in the middle of an active JavaScript stack. It waits until the current call has returned. That one rule is enough to explain both orders.

A tiny stack-and-messages modelPop out in the code editor (opens in a new tab)JavaScript
const stack = ["script"];const messages = []; function listener(number) {  console.log("listener " + number);  messages.push("microtask " + number);} listener(1);listener(2);while (messages.length) console.log(messages.shift());

Line 4 logs one listener. Line 5 queues its message. Lines 9 and 10 call both listeners before line 11 starts reading queued messages, so the output is listener 1, listener 2, microtask 1, microtask 2.

Real-life analogyPhone calls and messages

A script click is one long conference call with both listeners. A real user click is like separate calls: after one call ends, you can check messages before the next call begins.

In real life: One long conference call
In JavaScript: Script stays on the call stack
In real life: Two separate calls
In JavaScript: User input has separate listener boundaries
In real life: Checking messages after a call
In JavaScript: A microtask checkpoint runs
In real life: Messages wait while you are talking
In JavaScript: Microtasks do not interrupt active script

Where the analogy stops: Browsers have detailed event algorithms; the analogy only explains why an empty stack changes when queued messages can run.

The browser does not run a microtask simply because one was queued. It runs one at the checkpoint the surrounding dispatch reaches. This is why copying listener code alone does not predict order.

Microtask queue order is first in, first out for the work this lesson queues. If listener 1 queues its microtask before listener 2 queues one, microtask 1 is ahead of microtask 2 once the checkpoint begins. The puzzle is therefore about when draining starts, not which queued item overtakes another.

A long chain of microtasks can keep adding more microtasks. That is useful for small follow-up work, but it can delay a later timer and make an interface less responsive. Use this lesson's rule to understand that result: the browser does not move to the later task until the checkpoint is done.

The click-and-promise puzzle

Use the two replays to slow the puzzle down. They are instrumented teaching models that call the lesson's real order functions. They are not a browser engine debugger.

Step through a user-input boundary
Step 0 of 8Ready
Your turn: follow the blue line

Replay an instrumented teaching model of separate user-input listener boundaries. It is not a browser engine debugger.

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
 function listener(number) {  log.push("listener " + number);  queueMicrotask(() => log.push("microtask " + number));} // A user input boundary returns before the next listener starts.listener(1);// The stack is empty here, so the microtask checkpoint runs.listener(2); console.log(log);
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.

In the user-input model, listener 1 returns and the checkpoint runs microtask 1 before listener 2 starts. The same listener body is not special; the boundary around it is.

Step through button.click()
Step 0 of 7Ready
Your turn: follow the blue line

Replay an instrumented model of script dispatch. It shows why button.click() produces a different microtask boundary.

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
 function listener(number) {  log.push("listener " + number);  queueMicrotask(() => log.push("microtask " + number));} // Script stays on the stack while it calls both listeners.listener(1);listener(2);// The script call returns; now the microtask checkpoint runs. console.log(log);
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.

In the script model, the call to button.click() keeps the script active while it calls listener 1 and listener 2. The checkpoint appears only after that call returns, so both microtasks wait.

The replay intentionally shows state snapshots instead of pretending to pause Chrome. Each frame comes from a real lesson function and records the visible log or queue at that point. Back and Reset change the replay state, not the browser's actual event loop.

Try saying the two traces aloud. “Listener one returns, checkpoint, listener two returns, checkpoint” is the user model. “Script calls listener one, script calls listener two, script returns, checkpoint” is the synthetic model. A short sentence is often more reliable than a long diagram.

Playground: compare two click boundaries
The script click versionPop out in the code editor (opens in a new tab)JavaScript
const button = document.querySelector("#order-button"); button.addEventListener("click", () => {  console.log("listener 1");  queueMicrotask(() => console.log("microtask 1"));}); button.addEventListener("click", () => {  console.log("listener 2");  queueMicrotask(() => console.log("microtask 2"));}); button.click();
Computed orderuser
listener 1
microtask 1
listener 2
microtask 2
Try it yourself
Choose the click source

The user-input teaching model reaches a checkpoint after each listener: listener 1, microtask 1, listener 2, microtask 2.

The labels are computed by the lesson's dispatch-order functions. Use the visible buttons in the article examples for the browser's real event evidence.

Change one input in the playground: the click source. Reset restores the user-input model. The output is deliberately small so you can inspect the exact position of each microtask.

dispatchEvent is synchronous

Synchronous means the method does its listener work before returning to the next line. This is a useful rule for tests and application code because it is deterministic.

dispatchEvent() calls its listener before returningPop out in the code editor (opens in a new tab)JavaScript
const target = new EventTarget();target.addEventListener("order", () => console.log("listener")); console.log("before dispatch");target.dispatchEvent(new Event("order"));console.log("after dispatch");

Line 4 prints before dispatch. Line 5 calls the listener, which prints listener. Line 6 runs after dispatch returns. The output is before, listener, after.

Do not describe this as “the browser sends an event later.” `dispatchEvent()` is an immediate method call path. It can still queue microtasks, but those wait for the active script stack.

This makes `dispatchEvent()` valuable in unit tests: setup a target, call dispatch, and assert synchronous effects immediately. When the listener starts asynchronous work, write a second assertion after the appropriate promise or timer boundary. That keeps the test honest about what it waits for.

await inside a listener

An async listener starts synchronously. It runs until the first await, then its continuation is scheduled as a microtask. Other synchronous listeners can still run before that continuation.

await queues a continuationPop out in the code editor (opens in a new tab)JavaScript
const target = new EventTarget(); target.addEventListener("order", async () => {  console.log("listener 1");  await Promise.resolve();  console.log("after await");});target.addEventListener("order", () => console.log("listener 2")); target.dispatchEvent(new Event("order"));console.log("after dispatch");

Line 4 prints listener 1. Line 5 reaches await, so the async function pauses. Line 7 runs listener 2 during the same dispatch. Line 10 prints after dispatch, then the microtask prints after await.

This does not mean await makes an event listener run “later” from its first line. It splits one listener into a synchronous first part and a queued continuation.

In a form handler, that split often appears as immediate UI feedback followed by a request result. The first part can disable a button. The continuation can show a result once the promise settles. If a second listener needs to depend on the result, connect the dependency explicitly instead of assuming the continuation runs before it.

Timer versus promise

Promises and timers both delay work, but they use different queues. A promise reaction is a microtask. A zero-delay timer is a later timer task.

Promise beats a zero-delay timerPop out in the code editor (opens in a new tab)JavaScript
const target = new EventTarget(); target.addEventListener("order", () => {  console.log("listener");  Promise.resolve().then(() => console.log("promise"));  setTimeout(() => console.log("timer"), 0);}); target.dispatchEvent(new Event("order"));console.log("after dispatch");

Line 4 logs listener. Line 5 queues the promise callback. Line 6 queues a timer. After line 9 logs after dispatch, the microtask prints promise, and the later timer prints timer.

The number zero means the timer can become eligible soon. It does not let the timer jump ahead of the current stack or the microtask checkpoint that follows it.

Eligibility is not a clock guarantee. Browser scheduling, nested-timer clamping, background throttling, and other tasks can delay a timer. The stable lesson fact is only the ordering boundary here: a promise reaction queued in this stack runs before the later timer task.

Trusted events

Trusted means the browser created the event from real user interaction. A script can ask an element to click, but it cannot turn that event into trusted user input.

A script click is untrustedPop out in the code editor (opens in a new tab)JavaScript
const button = document.querySelector("#order-button");button.addEventListener("click", (event) => console.log(event.isTrusted)); button.click();

Line 2 prints the event's trust flag. Line 4 calls the click from script, so it prints false. A physical browser click produces an event whose isTrusted is true.

Trust is not an ordering tool and not a security permission you should invent yourself. It tells you something about the event source, which matters when code must distinguish a user gesture from a synthetic test action.

Most application logic should work for both trusted and untrusted events. A test that calls `click()` should still validate a checkout calculation or a selected tab. Reserve trust checks for a requirement that genuinely depends on a user gesture, and test that requirement in a real browser.

Ordering puzzles

Ordering questions become easier when you label every line: current stack, microtask, or later task. Do not start by memorizing a long list of APIs.

Place each action in its queue
  • A listener called by dispatchEvent()
  • The line after target.dispatchEvent(event)
  • A Promise.resolve().then(...) callback
  • Code after await Promise.resolve()
  • A setTimeout(..., 0) callback
  • The next physical click from a person
Try it yourself
0 of 6 correct

Sort each card by when it can run. Read the explanation after every placement.

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

A listener called by dispatchEvent() and the line after it are synchronous. Promise reactions and code after an already-resolved await are microtasks. Timers and future physical clicks are later tasks.

This classification is also a debugging method. Add temporary logs that name the boundary, then compare the observed order with the boundary you expected.

Keep the labels modest. “sync start”, “promise checkpoint”, and “timer task” are enough. Names such as “fast” and “slow” are not useful because a microtask can be delayed by active script and a timer can run quickly on an idle page. The queue explains more than a vague speed label.

Practical event code

Real interfaces often have analytics, validation, and UI updates listening for the same event. Do not make correctness depend on whether the event came from a person, a test, or a helper that calls click().

When order matters, make it explicit. Put dependent work in one handler, call a named function in sequence, or await the operation that must finish first. Then a synthetic test and a real interaction share the same business rule.

  • Use a single named flow for work that must happen in order.
  • Use microtasks for short follow-up work, not as a hidden coordination channel.
  • Use timers only when a later task is actually part of the requirement.
  • Test trusted-input behavior separately from ordinary application ordering.

Related API lessons explain listener registration, propagation, and custom events: Events introduction, bubbling and capturing, and custom events.

For a production bug report, record the source of the event, the listener names, and the queue boundary before proposing a fix. A one-line reproduction that uses `dispatchEvent()` can reveal whether the bug is in your business sequence or only in a physical-input path.

Common misconceptions

  • “All clicks are the same.” Their source can change trust and microtask boundaries.
  • “A microtask runs right after I queue it.” It waits for the active stack to empty.
  • “`await` pauses every listener.” It pauses only that async function after its first await.
  • “Zero-delay timers are immediate.” They run in a later task.
  • “`dispatchEvent()` creates user input.” It synchronously creates a synthetic dispatch.
Similar-looking ideas to keep separate
IdeaWhat it meansNot the same as
click()A synchronous script-dispatched click with isTrusted === false.A physical user click.
A promise callbackA microtask that waits for the current stack to empty.A timer callback or a parallel thread.
awaitA pause for the async function; its continuation is queued as a microtask.A pause for other listeners already being dispatched.
dispatchEvent()A synchronous listener call mechanism.A way to create browser user input.

Practice exercises

Exercise 1 · Warm-upPredict synchronous dispatch

Read the tiny event target program. Type its three outputs in order, separated by commas.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const target = new EventTarget();
target.addEventListener("order", () => console.log("listener"));
console.log("before");
target.dispatchEvent(new Event("order"));
console.log("after");

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

    Exercise 2 · Warm-upPredict a script click

    Two listeners each queue one microtask. What order does a script call to button.click() produce?

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

      Exercise 3 · PracticeSplit an async listener

      The program logs start, outside, then one more word. What word prints after outside?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const target = new EventTarget();
      target.addEventListener("order", async () => {
        console.log("start");
        await Promise.resolve();
        console.log("later");
      });
      target.dispatchEvent(new Event("order"));
      console.log("outside");

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

        Exercise 4 · PracticeChoose the last callback

        Which word prints last when one listener queues both a promise callback and a zero-delay timer?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const target = new EventTarget();
        target.addEventListener("order", () => {
          Promise.resolve().then(() => console.log("promise"));
          setTimeout(() => console.log("timer"), 0);
        });
        target.dispatchEvent(new Event("order"));
        console.log("sync");

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

          Exercise 5 · PracticeCheck a synthetic event

          Your test calls button.click(). What does its event's isTrusted property say?

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

            Exercise 6 · ChallengeApply it to a checkout button

            A checkout button starts validation and analytics in separate listeners, but a test uses click() and sees a different order. What should the handler design do instead?

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

              Check your understanding

              For every answer, identify the event source and then locate the first empty-stack checkpoint.

              Event dispatch ordering quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Why can the same two listeners produce different orders?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const target = new EventTarget();
                target.addEventListener("order", () => console.log("listener"));
                console.log("before");
                target.dispatchEvent(new Event("order"));
                console.log("after");

                Choose an answer to see the explanation.

              3. Question 3 of 7Where does code after await Promise.resolve() run?

                Choose an answer to see the explanation.

              4. Question 4 of 7What prints last?

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

                Choose an answer to see the explanation.

              5. Question 5 of 7What is event.isTrusted for button.click()?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which API calls listeners before returning?

                Choose an answer to see the explanation.

              7. Question 7 of 7What is the safest way to depend on ordering in app code?

                Choose an answer to see the explanation.

              Key takeaways

              • The source of an event can change dispatch boundaries and isTrusted.
              • Microtasks run when the current JavaScript stack reaches a checkpoint.
              • button.click() and dispatchEvent() call listeners synchronously.
              • Code after await and promise reactions are microtasks; timers are later tasks.
              • Make business ordering explicit instead of depending on event-source details.

              Remember the one-liner.
              Event order follows dispatch boundaries and microtask checkpoints, not just the listener code you can see.

              Coming next: Timers in depth, where zero delay, clamping, and timer guarantees get the same careful treatment.

              CompleteFrontend Clear concepts. Working examples.