Events, microtasks & ordering
Learn why real clicks and script-dispatched events can run listeners and microtasks in different orders in JavaScript.
- 01Follow a clickPredict when listeners and microtasks run by asking one question: when is the stack empty?
- 02Tell clicks apartExplain why a real click,
click(), anddispatchEvent()can run microtasks at different moments. - 03Pick the right queueSay when promises, code after
await, and timers run, and whatisTrustedtells you, without guessing.
One click, two possible orders
A click can be started in two ways:
- A person clicks the button with the mouse or the keyboard. The browser creates the click.
- Your code calls
button.click(). JavaScript that is already running creates the click.
Both clicks call the same listeners. A listener is a function you attach to an element so it runs when an event happens. But the two clicks can print the listeners' logs in a different order. This lesson shows why.
Event dispatch means the browser, or your code, calling the listeners for an event. A microtask is a small piece of work that waits in a line, called the microtask queue. A promise callback is one kind of microtask. Microtasks run at a checkpoint: a moment when no JavaScript is running.
Here is the puzzle. There are two listeners. Each listener prints one line and then adds one microtask to the queue. Then:
- A real click prints: listener 1, microtask 1, listener 2, microtask 2.
- A
button.click()call prints: listener 1, listener 2, microtask 1, microtask 2.
button.click(), both microtasks wait until both listeners are done.No promise is "racing" here. The answer depends on one question: is any JavaScript still running? The call stack, or just the stack, is the list of functions that are running right now. A microtask can only run when the stack is empty.
This question also helps when you debug. Two log lines can sit next to each other in the console and still come from different places. One may be normal code, one a microtask, and one a later task. Before you change any code, write down where each line came from.
Your app should not depend on these small details to work correctly. This lesson explains what the browser does, so that a failing test or a surprising log makes sense. Your own code can still choose a clear order on purpose.
A real click and a script click
Start with the script version. It is short, and you can run it. Two new tools appear in it:
queueMicrotask(fn)adds the functionfnto the microtask queue. It does not run it yet.button.click()clicks the button from code. It calls the button's click listeners right away.
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();Lines 3 to 6 add listener 1. Lines 8 to 11 add listener 2. Each listener prints a line and queues one microtask. Line 13 calls button.click().
It prints listener 1, listener 2, microtask 1, microtask 2. Line 13 does not finish until both listeners have run. The stack is busy the whole time. Only when line 13 is done and the script ends can the microtasks run.
Now add the same two listeners and click the button yourself. The listeners are exactly the same. The only change is who starts the click: you, through the browser.
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.In Chrome, a real click prints listener 1, microtask 1, listener 2, microtask 2. This lesson's test checks that with a real mouse click in Chrome. It checks the button.click() order too. Each order belongs to its own kind of click.
The order in which you add listeners still matters. Listener 1 was added first, so it runs first in both versions. The only difference is when the microtasks get their turn.
Pressing Enter or Space on a focused button is also a real click from a person. You can test it separately if a control has keyboard behavior. This lesson uses one button and the mouse, so the only change is who starts the click.
The table compares four things that look alike. Read each row as: what it is, who starts it, and what order you see.
| What you use | Who starts it? | What order do you see? |
|---|---|---|
| A real click from a person | The browser starts it. It runs each listener on its own, with an empty stack in between. | Listener 1, microtask 1, listener 2, microtask 2, in Chrome. |
button.click() | Your running script starts it. The click is not trusted. | Listener 1, listener 2, microtask 1, microtask 2. |
dispatchEvent(event) | Your running script starts it. It runs the listeners before it returns. | The listeners run first. The next line waits until they are done. |
| A queued microtask | Your code queues it. It waits until the stack is empty. | It runs before a later timer, never in the middle of running code. |
Why a microtask can run between two listeners
A microtask never runs in the middle of other code. It waits until the stack is empty. This one rule explains both orders.
The code below is a tiny model of that rule. It uses an array, messages, as a pretend microtask queue. messages.shift() takes out the first item in the array.
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 5 prints one listener line. Line 6 adds its message to the queue. Lines 9 and 10 call both listeners. Only then does line 11 read the waiting messages, one by one. So the output is listener 1, listener 2, microtask 1, microtask 2. That is the button.click() order.
While you are on a call, new messages wait. You read them only after you hang up. A script click is one long call with both listeners, so you read the messages at the end. A real click is two short calls. Between them, you hang up and read your messages.
- In real life: One long call with both people
- In JavaScript:
button.click()keeps the stack busy for both listeners - In real life: Two short calls, one after the other
- In JavaScript: A real click runs each listener on its own
- In real life: You check your messages after you hang up
- In JavaScript: Microtasks run when the stack is empty
- In real life: Messages wait while you are talking
- In JavaScript: Microtasks never cut into running code
So queueing a microtask does not make it run right away. It runs at the next checkpoint. That is why copying the listener code alone cannot tell you the order. You also need to know who started the click.
Microtasks run in the order they were queued: first in, first out. Listener 1 queues microtask 1 before listener 2 queues microtask 2. So microtask 1 always runs before microtask 2. The puzzle is only about when the checkpoint happens.
A microtask can queue another microtask. All of them run before the browser moves on to the next task, such as a timer. So a long chain of microtasks can delay a timer and make the page slow to respond. Keep microtask work short.
Step through the puzzle
The two step-throughs below slow the puzzle down. Each one is a small model written in plain JavaScript. It uses the lesson's real order functions. It is not the browser's own debugger.
Step through a small model of a real click, where each listener runs on its own. It is a model in plain JavaScript, not the browser's debugger.
script
function listener(number) { log.push("listener " + number); queueMicrotask(() => log.push("microtask " + number));} async function runUserModel() { listener(1); await Promise.resolve(); listener(2); await Promise.resolve(); console.log(log.join(", "));}runUserModel();In the real-click model, listener 1 finishes. Then the checkpoint runs microtask 1. Only then does listener 2 start. The listener code is nothing special. What changes is the empty stack between the two listeners.
Step through a small model of button.click(). It shows why both microtasks wait until both listeners are done.
script
function listener(number) { log.push("listener " + number); queueMicrotask(() => log.push("microtask " + number));} listener(1);listener(2);queueMicrotask(() => console.log(log.join(", ")));In the script model, the call to button.click() stays on the stack while it calls listener 1 and listener 2. The checkpoint comes only after that call returns. So both microtasks wait.
Each step shows the log or the queue at that moment. Back and Reset move through the recorded steps. They do not change the browser itself.
Try saying the two orders out loud:
- Real click: "listener 1 ends, checkpoint, listener 2 ends, checkpoint."
- Script click: "the script calls listener 1, then listener 2, then ends, then checkpoint."
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 2A real click: the stack is empty after each listener, so each microtask runs right after its own listener. Listener 1, microtask 1, listener 2, microtask 2.
In the playground, change one thing: who starts the click. Reset goes back to the real click. The output is short, so you can see exactly where each microtask lands.
dispatchEvent runs listeners right away
target.dispatchEvent(event) sends an event to a target from your code. It is synchronous: it runs all the matching listeners before it returns to the next line. The code below also uses two new tools:
new EventTarget()makes a plain object that can have listeners. It works like a button with no page around it.new Event("order")makes an event namedorder.
dispatchEvent() runs its listener before it returnsconst target = new EventTarget();target.addEventListener("order", () => console.log("listener")); console.log("before dispatch");target.dispatchEvent(new Event("order"));console.log("after dispatch");Line 2 adds a listener for order. Line 4 prints before dispatch. Line 5 calls the listener, which prints listener. Line 6 prints after dispatch. So the output is before dispatch, listener, after dispatch.
dispatchEvent() runs the listener before it returns. So listener prints between the two outer lines.So dispatchEvent() does not "send the event later". It calls the listeners straight away, like a normal function call. A listener can still queue a microtask. That microtask waits until the stack is empty.
This makes dispatchEvent() useful in tests:
- Set up a target and its listener.
- Call
dispatchEvent(). - Check the listener's effects on the very next line.
If the listener starts work that finishes later, wait for that promise or timer before you check the result. Then the test says clearly what it waits for.
await inside a listener
A listener can be an async function. An async function runs right away, line by line, until it reaches its first await. There it pauses. The rest of the function goes into the microtask queue. Other listeners can run before it continues.
Promise.resolve() makes a promise that is already finished. So await Promise.resolve() waits for nothing, but the rest of the function still has to wait for the next checkpoint.
await puts the rest of the listener in the queueconst 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 10 dispatches the event. Line 4 prints listener 1. Line 5 reaches await, so the first listener pauses. The listener from line 8 runs and prints listener 2, in the same dispatch. Line 11 prints after dispatch. Now the stack is empty, so the microtask runs line 6 and prints after await.
So await does not make the whole listener run later. It splits the listener in two:
- The part before
awaitruns right away. - The part after
awaitwaits in the microtask queue.
You often see this split in a form handler. The first part runs right away and can turn off the submit button. The second part runs when the request is done and shows the result. If another listener needs that result, connect the two on purpose. Do not hope the second part runs before the other listener.
A timer and a promise
Promises and timers both run code later. But they wait in different queues:
- A promise callback, such as the function you pass to
.then(), is a microtask. setTimeout(fn, 0)runsfnafter a delay of 0 milliseconds. The browser runs it in a later task: a separate turn of the event loop, after all microtasks are done.
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 9 dispatches the event. Line 4 prints listener. Line 5 queues the promise callback. Line 6 queues the timer. Line 10 prints after dispatch. Now the stack is empty, so the microtask prints promise. The timer runs last, in a later task, and prints timer.
A delay of 0 means the timer may run soon. It does not let the timer jump ahead of code on the stack, or ahead of the microtasks that run after it.
"May run soon" is not a promise about the clock. The browser can delay a timer for many reasons:
- Other tasks are waiting first.
- Timers nested inside other timers get a small minimum delay.
- A tab in the background runs timers less often.
The one fact you can rely on here is the order. A promise callback queued by this code runs before the later timer task.
Trusted events
Every event has an isTrusted property. It is true when the browser created the event from a real action by a person. A script can ask a button to click, but that click is never trusted.
const button = document.querySelector("#order-button");button.addEventListener("click", (event) => console.log(event.isTrusted)); button.click();Line 2 adds a listener that prints event.isTrusted. Line 4 clicks from code, so it prints false. If you click the button yourself, the browser creates the event, and it prints true.
isTrusted does not change the order of anything. It is not a security check you should build on either. It only tells you where the event came from: a person, or code. That matters when code must tell a real person's action apart from a test.
Most app code should work the same for both kinds of event. A test that calls click() should still check the checkout total or the chosen tab. Only check isTrusted when a feature really needs a real person's action, and test that feature in a real browser.
Ordering puzzles
Ordering questions get easier when you give every line one of three labels:
- runs now, on the current stack;
- a microtask, which waits for the stack to be empty;
- a later task, such as a timer.
You do not need to learn a long list of functions by heart.
- 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 real click from a person
Sort each card by when it can run. Read the explanation after each one.
A listener called by dispatchEvent(), and the line after it, run now. Promise callbacks, and code after an await of a finished promise, are microtasks. Timers, and the next real click from a person, are later tasks.
The same labels help you debug. Add short logs that name each label. Then compare the order you see with the order you expected.
Keep the log names plain, such as "sync start", "promise checkpoint", and "timer task". Avoid names like "fast" and "slow". Busy code can delay a microtask, and a timer can run quickly on a quiet page. The queue tells you more than a guess about speed.
Event order in real code
Real pages often have several listeners for the same event: one for analytics, one to check the form, one to update the page. Your code should work the same whether the click came from a person, a test, or a helper that calls click().
When order matters, write the order down in code:
- Put the steps that depend on each other in one listener.
- Or call named functions one after another.
- Or
awaitthe work that must finish first.
Then a test click and a real click follow the same steps. Some rules of thumb:
- Use one named function for work that must happen in order.
- Use microtasks for short follow-up work, not as a hidden way to order steps.
- Use timers only when you really need the work to happen in a later task.
- Test features that need a real person's click on their own, apart from the normal order checks.
These lessons explain adding listeners, how events travel through the page, and events you create yourself: Events introduction, bubbling and capturing, and custom events.
When you report an order bug, write down three things before you suggest a fix: who started the event, the names of the listeners, and where each log runs (now, microtask, or later task). A short example that uses dispatchEvent() can show whether the bug is in your own steps or only appears with a real click.
Common misconceptions
- "All clicks are the same." Who starts a click can change
isTrustedand when microtasks run. - "A microtask runs as soon as I queue it." It waits until the stack is empty.
- "
awaitpauses every listener." It pauses only its ownasyncfunction, at thatawait. - "A timer with 0 delay runs right away." It runs in a later task.
- "
dispatchEvent()acts like a person clicking." It calls the listeners right away from your code, and the event is not trusted.
The table below lists four ideas that are easy to mix up. Each row says what the idea is, and what it is not.
| The idea | What is it? | What is it not? |
|---|---|---|
click() | A click made by code. It runs the listeners right away, and isTrusted is false. | It is not a real click by a person. |
| A promise callback | A microtask. It waits until the stack is empty. | It is not a timer, and it does not run at the same time as other code. |
await | A pause in its own async function. The rest of that function waits in the microtask queue. | It does not pause the other listeners of the same event. |
dispatchEvent() | A way to run an event's listeners right away from your code. | It is not a way to make a real person's click. |
Practice exercises
Read the small program. Type its three outputs in order, with commas between them.
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. dispatchEvent() calls the listener right away, between the two outer logs.
Two listeners each queue one microtask. What order does a script call to button.click() print?
button.click() prints listener 1, listener 2, microtask 1, microtask 2. The click stays on the stack through both listeners, so both microtasks wait.
The program prints start, then 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 right away, pauses at await, and finishes in a microtask.
One listener queues a promise callback and a timer with 0 delay. Which word prints last?
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. sync prints first, then the promise callback runs at the checkpoint, then the timer runs in a later task.
Your test calls button.click(). What does the event's isTrusted property say?
For button.click(), isTrusted is false. Only an event the browser creates from a real person's action is trusted.
A checkout button checks the form in one listener and sends analytics in another. A test uses click() and sees a different order than a real click. What should the code do instead?
Make the order explicit: use one listener, or call named steps one after another, and await any work that must finish first. Then the order no longer depends on who clicked.
Check your understanding
For each question, first ask who started the event. Then find the first moment when the stack is empty.
Question 1 of 7Why can the same two listeners print in 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 one runs listeners before it returns?
Choose an answer to see the explanation.
Question 7 of 7What is the safest way to get a fixed order in app code?
Choose an answer to see the explanation.
Key takeaways
- Who starts an event can change when microtasks run, and the value of
isTrusted. - Microtasks run only when the stack is empty, at a checkpoint.
button.click()anddispatchEvent()call the listeners right away, before they return.- Promise callbacks and code after
awaitare microtasks. Timers run in a later task. - When order matters in your app, write that order in your code. Do not depend on who started the click.
Remember the one-liner.
Event order depends on when the stack becomes empty, not only on the listener code you can see.
Coming next: Timers in depth, which looks closely at zero delay, minimum delays, and what a timer really promises.