denoland/deno · error · NodeError

ERR_EVENT_RECURSION

ERR_EVENT_RECURSION

Error message

The event "${event.type}" is already being dispatched

What it means

Node keeps a [kIsBeingDispatched] flag on each event; dispatchEvent refuses any event whose flag is still set, throwing ERR_EVENT_RECURSION. This happens when the exact same Event instance is re-dispatched synchronously while its own listeners are still running. Dispatching a fresh event with the same type is always legal.

Source

Thrown at ext/node/polyfills/internal/event_target.mjs:708

      }
      handler = handler.next;
    }
  }

  /**
   * @param {Event} event
   */
  dispatchEvent(event) {
    if (!isEventTarget(this)) {
      throw new ERR_INVALID_THIS("EventTarget");
    }

    if (!ObjectPrototypeIsPrototypeOf(globalThis.Event.prototype, event)) {
      throw new ERR_INVALID_ARG_TYPE("event", "Event", event);
    }

    if (event[kIsBeingDispatched]) {
      throw new ERR_EVENT_RECURSION(event.type);
    }

    this[kHybridDispatch](event, event.type, event);

    return event.defaultPrevented !== true;
  }

  [kHybridDispatch](nodeValue, type, event) {
    const createEvent = () => {
      if (event === undefined) {
        event = this[kCreateEvent](nodeValue, type);
        event[kTarget] = this;
        event[kIsBeingDispatched] = true;
      }
      return event;
    };
    if (event !== undefined) {
      event[kTarget] = this;

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Dispatch a fresh instance: dispatchEvent(new Event(e.type)) instead of e
  2. For CustomEvent payloads: dispatchEvent(new CustomEvent(e.type, { detail: e.detail }))
  3. If re-dispatch must happen in the listener, defer it: queueMicrotask(() => target.dispatchEvent(new Event(e.type)))
  4. Audit listeners that forward the event they received into another dispatchEvent

Example fix

// before
target.addEventListener('tick', (e) => {
  target.dispatchEvent(e); // same instance, still dispatching
});

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

Strategy: validation

Validate before calling

function safeDispatch(target, type, detail) {
  // always build a fresh event: a cached/in-flight instance would throw ERR_EVENT_RECURSION
  const ev = detail === undefined ? new Event(type) : new CustomEvent(type, { detail });
  target.dispatchEvent(ev);
}

Try / catch

try { target.dispatchEvent(e); } catch (err) { if (err?.code === 'ERR_EVENT_RECURSION') { queueMicrotask(() => target.dispatchEvent(new Event(e.type))); return; } throw err; }

Prevention

When it happens

Trigger: Inside a listener for event e, calling target.dispatchEvent(e) with the same object; replay utilities that cache the last real event and re-dispatch the cached instance; retry logic inside a listener that re-fires the triggering event; forwarding pipelines that pass the received event object to another dispatchEvent call.

Common situations: Event-replay and mocking helpers that store live events; middleware that wraps dispatchEvent and re-dispatches the same instance; one CustomEvent object hoisted to module scope and reused for every emit.

Related errors


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