Events, microtasks & ordering
Learn why real clicks and script-dispatched events can run listeners and microtasks in different orders in JavaScript.
- 01Trace a clickPredict listener and microtask order by asking when the current call stack becomes empty.
- 02Separate event sourcesExplain why user input,
click(), anddispatchEvent()do not create the same dispatch boundary. - 03Choose the next queueReason about promises,
await, timers, andisTrustedwithout 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.
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.
button.click()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.
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.
| Source | Who starts it? | Observed order |
|---|---|---|
| A real user click | The 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 microtask | A 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.
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.
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.
Replay an instrumented teaching model of separate user-input listener boundaries. It is not a browser engine debugger.
script
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);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.
Replay an instrumented model of script dispatch. It shows why button.click() produces a different microtask boundary.
script
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);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.
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();listener 1microtask 1listener 2microtask 2The user-input teaching model reaches a checkpoint after each listener: listener 1, microtask 1, listener 2, microtask 2.
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 returningconst 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 continuationconst 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.
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.
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.
- 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
Sort each card by when it can run. Read the explanation after every placement.
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.
| Idea | What it means | Not the same as |
|---|---|---|
click() | A synchronous script-dispatched click with isTrusted === false. | A physical user click. |
| A promise callback | A microtask that waits for the current stack to empty. | A timer callback or a parallel thread. |
await | A 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
Read the tiny event target program. Type its three outputs in order, separated by commas.
const target = new EventTarget();
target.addEventListener("order", () => console.log("listener"));
console.log("before");
target.dispatchEvent(new Event("order"));
console.log("after");The output is before, listener, after. The dispatch call synchronously invokes the listener between the two outer logs.
Two listeners each queue one microtask. What order does a script call to button.click() produce?
button.click() produces listener 1, listener 2, microtask 1, microtask 2. Its synchronous dispatch stays on the stack through both listeners.
The program logs start, outside, then one more word. What word prints after outside?
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");outside prints before later. The async listener starts immediately, pauses at await, and resumes in a microtask.
Which word prints last when one listener queues both a promise callback and a zero-delay timer?
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");timer prints last. The synchronous log happens first, the promise callback runs at the checkpoint, and the timer is a later task.
Your test calls button.click(). What does its event's isTrusted property say?
For button.click(), isTrusted is false. A real user input event is the trusted case.
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?
Make the order explicit: use one listener or a named sequence and await any required asynchronous work. Then the flow does not depend on how the click arrived.
Check your understanding
For every answer, identify the event source and then locate the first empty-stack checkpoint.
Question 1 of 7Why can the same two listeners produce different orders?
Choose an answer to see the explanation.
Question 2 of 7What does this print?
Read the code, then predictconst 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.
Question 3 of 7Where does code after
await Promise.resolve()run?Choose an answer to see the explanation.
Question 4 of 7What prints last?
Read the code, then predictPromise.resolve().then(() => console.log("promise")); setTimeout(() => console.log("timer"), 0); console.log("sync");Choose an answer to see the explanation.
Question 5 of 7What is
event.isTrustedforbutton.click()?Choose an answer to see the explanation.
Question 6 of 7Which API calls listeners before returning?
Choose an answer to see the explanation.
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()anddispatchEvent()call listeners synchronously.- Code after
awaitand 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.