When you call el.dispatchEvent(new CustomEvent('save')), have the listeners already run by the time the next line of your function executes, and what does dispatchEvent return?
answer
- no queue is involved
- the next line waits
- a throwing listener does not reach your catch
- the boolean answers one narrow question
- meaningless unless the event opted in
basics
~10 sDispatch is fully synchronous: dispatchEvent invokes every matching listener and only then returns, before the next statement runs. It returns false when the event was cancelable and a listener called preventDefault, and true otherwise.
solid answer
~50 s`dispatchEvent` is a blocking call, not a queue push. It walks the propagation path and calls every matching listener synchronously, so by the time control reaches your next statement all of them have finished — there is no task and no microtask involved. That has real consequences: a slow listener lengthens your call, a listener can dispatch further events that complete before the outer one resumes, and an exception thrown inside a listener does not escape to your `try/catch` — the browser reports it as an uncaught error and keeps calling the remaining listeners. The boolean return value answers exactly one question: was the event cancelled? It is `false` if the event was constructed with `cancelable: true` and some listener called `preventDefault()`, `true` in every other case — including when there were no listeners at all. A custom event has no built-in default action, so cancellation only means something if the dispatching code actually checks that return value and acts on it.
code
javascript · 16 linesconst row = document.createElement('div');
document.body.append(row);
row.addEventListener('row:before-delete', (event) => {
if (event.detail.id === 1) event.preventDefault();
});
function tryDelete(id) {
const allowed = row.dispatchEvent(
new CustomEvent('row:before-delete', { detail: { id }, cancelable: true })
);
console.log(id, allowed ? 'deleted' : 'vetoed by a listener');
}
tryDelete(1); // 1 vetoed by a listener
tryDelete(2); // 2 deletedgo deeper
Know that dispatchEvent calls the listeners immediately, in the same turn — the line after it runs only once every listener has returned.
Explain the return value precisely: false only when the event was cancelable and a listener called preventDefault, true otherwise, including with zero listeners.
Reason about the consequences in production: a slow listener extends your call, nested dispatches recurse, listener exceptions never reach your catch, and async listeners cannot participate in a cancellation decision.
Decide when the synchronous model is the wrong tool — when subscribers need to do asynchronous work or must not be able to block the emitter, design an explicit async coordination contract instead of leaning on DOM dispatch.
## Dispatch blocks A very common mental model is that dispatching an event schedules it. It does not. `EventTarget.dispatchEvent()` performs the whole dispatch inline: it computes the propagation path, invokes each matching listener in registration order at each step, and returns only when the last one has returned. ```js console.log('before'); el.addEventListener('save', () => console.log('listener')); el.dispatchEvent(new CustomEvent('save')); console.log('after'); // before, listener, after ``` No timer, no microtask, no yielding to the browser. If a listener spends 200 ms doing work, your call to `dispatchEvent` took 200 ms and the frame it was on is that much longer. ## Why it matters in practice **Re-entrancy.** A listener can dispatch another event, or call code that does. That inner dispatch runs to completion first, and only then does the outer dispatch continue with its remaining listeners. Two components that each emit in response to the other's events can therefore recurse until the stack overflows, with no queue to smooth it out. **Ordering assumptions.** Because listeners at the same node run in registration order, dispatch order depends on when each listener was attached — which in a lazily-loaded app can vary between page loads. Never encode business logic in listener ordering. **Exception isolation.** If a listener throws, the exception is *reported* — it surfaces as an uncaught error and fires `window`'s `error` event — but it is not rethrown to the dispatcher, and the remaining listeners still run. So this catches nothing: ```js try { el.dispatchEvent(new CustomEvent('save')); } catch (e) { // never reached for an exception thrown inside a listener } ``` That isolation is deliberate: one broken subscriber must not break the emitter or its peers. It also means an emitter cannot learn that a listener failed. If you need that, listeners have to report failure through the payload rather than by throwing. **async listeners.** A listener declared `async` returns a promise the dispatch machinery ignores. Its synchronous prefix runs during dispatch; everything after the first `await` runs later. So `dispatchEvent` returning does *not* mean the listeners' asynchronous work finished, and there is no built-in way to await it. ## The return value `dispatchEvent` returns a boolean with exactly one meaning: `false` if the event's `cancelable` flag was set *and* a listener called `preventDefault()`; `true` otherwise. Note what that implies — if you construct the event without `cancelable: true`, a listener calling `preventDefault()` has no effect and the return stays `true`. And with no listeners registered at all, the return is `true`, so the value tells you nothing about whether anyone was listening. ## The cancellable-custom-event pattern Custom events have no default action for the browser to suppress, so cancellation is purely a convention between your emitter and its subscribers. The emitter opts in and then *honours* the result: ```js const allowed = row.dispatchEvent( new CustomEvent('row:before-delete', { detail: { id }, cancelable: true, bubbles: true }) ); if (allowed) deleteRow(id); ``` This is the platform-native equivalent of a veto hook: any subscriber can call `event.preventDefault()` to stop the deletion, and the emitter decides what the veto means. If the emitter ignores the return value, `cancelable: true` is decorative — a subscriber will call `preventDefault()`, nothing will happen, and the bug is invisible. Because the veto must be decided before the emitter continues, this pattern only works *because* dispatch is synchronous — a subscriber cannot `await` anything and still cancel in time. That is the practical reason to know the timing rule, not just trivia. ## One dispatch at a time An event object carries an internal dispatch flag while it is in flight. Calling `dispatchEvent` with an event that is already being dispatched throws an `InvalidStateError` `DOMException`. Re-dispatching an event object after its dispatch has finished is allowed, but constructing a fresh event is clearer and avoids carrying stale state such as `defaultPrevented`.
- A listener throws. Does your try/catch around dispatchEvent catch it?No. The exception is reported as an uncaught error — it fires `window`'s `error` event — rather than propagating to the dispatcher, and the remaining listeners still run. That isolation keeps one bad subscriber from breaking the emitter, but it also means an emitter cannot detect listener failure; failures have to be signalled through the payload instead.
- If a listener is declared async, does dispatchEvent wait for it?No. An async listener returns a promise that dispatch ignores. Only the code before its first `await` runs during dispatch; the rest resumes later. So `dispatchEvent` returning true tells you nothing about asynchronous work still in flight, and there is no built-in way to await all listeners.
- dispatchEvent returned true. Does that mean a listener handled the event?No. `true` only means the event was not cancelled — which is also the answer when no listener existed at all, or when the event was not constructed with `cancelable: true`. The DOM gives you no way to ask whether anyone was subscribed; if you need that, have handlers record it in `detail`.
- What does cancelable: true actually buy you on a custom event?Nothing by itself — a custom event has no browser default action. It enables a veto convention: a subscriber may call `preventDefault()`, which makes `dispatchEvent` return `false`, and the emitter chooses to respect that by branching on the return value. Without the emitter's check, the flag is decorative.
saying these in an interview costs you the question
- Thinks dispatchEvent queues the event like setTimeout
- Wraps dispatchEvent in try/catch expecting to catch listener errors
- Reads the return value as "a listener handled it"
- Calls preventDefault on an event that was never cancelable
- Awaits dispatchEvent expecting async listeners to finish