denoland/deno · error · DOMException

InvalidStateError

InvalidStateError

Error message

Invalid event state

What it means

EventTarget.prototype.dispatchEvent throws DOMException 'Invalid event state' (InvalidStateError) when the event's internal [[dispatched]] flag is set — i.e. the same Event object is dispatched again while still inside dispatch() (checked at ext/web/02_event.js:1126). In Deno's implementation the flag is set on entry to dispatch() (ext/web/02_event.js:571) and cleared only when the dispatch completes normally (ext/web/02_event.js:711), so it fires for synchronous re-dispatch from within one of the event's own listeners, and also when a listener threw during an earlier dispatch of the same object and left the flag permanently set. This matches the DOM spec rule that an event must never be dispatched while its dispatch flag is active.

Source

Thrown at ext/web/02_event.js:1127

    );

    // This is an optimization to avoid creating an event listener
    // on each startup.
    // Stores the flag for checking whether unload is dispatched or not.
    // This prevents the recursive dispatches of unload events.
    // See https://github.com/denoland/deno/issues/9201.
    if (event.type === "unload" && self === globalThis_) {
      globalThis_[SymbolFor("Deno.isUnloadDispatched")] = true;
    }

    const data = self[eventTargetData];
    if (data === undefined || !data.listeners[event.type]) {
      setTarget(event, this);
      return true;
    }

    if (getDispatched(event)) {
      throw new DOMException("Invalid event state", "InvalidStateError");
    }

    if (event.eventPhase !== Event.NONE) {
      throw new DOMException("Invalid event state", "InvalidStateError");
    }

    return dispatch(self, event);
  }

  getParent(_event) {
    return null;
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return `${this.constructor.name} ${inspect({}, inspectOptions)}`;
  }
}

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Do not re-dispatch the event object itself; forward its payload to a fresh event: hub.dispatchEvent(new CustomEvent(e.type, { detail: e.detail })).
  2. Defer the second dispatch with queueMicrotask() so the first dispatch finishes and clears the flag first.
  3. If the error appears on a later dispatch after a listener threw, the event object is poisoned — create a new Event every time.
  4. Audit addEventListener callbacks for dispatchEvent calls that pass through their own event argument.

Example fix

// before
bus.addEventListener('ping', (e) => hub.dispatchEvent(e)); // e is mid-dispatch -> Invalid event state
bus.dispatchEvent(new Event('ping'));

// after
bus.addEventListener('ping', (e) =>
  hub.dispatchEvent(new CustomEvent(e.type, { detail: e.detail }))
);
Defensive patterns

Strategy: try-catch

Try / catch

try {
  target.dispatchEvent(evt);
} catch (e) {
  if (e instanceof DOMException && e.name === 'InvalidStateError') {
    target.dispatchEvent(new Event(evt.type, { bubbles: evt.bubbles, cancelable: evt.cancelable }));
  } else throw e;
}

Prevention

When it happens

Trigger: bus.addEventListener('ping', (e) => hub.dispatchEvent(e)); bus.dispatchEvent(new Event('ping')); — the live event object is re-dispatched synchronously inside its own listener. Also: a listener threw during the first dispatch (the reset at ext/web/02_event.js:708-713 never ran), so every later dispatchEvent(sameEvent) throws InvalidStateError forever.

Common situations: Middleware/event-bus designs that forward the live event object to a central EventTarget synchronously; fan-out code that re-dispatches a captured event; test harnesses reusing one Event instance across listeners; migrating from Node EventEmitter where emit is freely re-callable.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/1bf6a99ffd6604b1. Report an issue: GitHub.