denoland/deno · error · DOMException

InvalidStateError

InvalidStateError

Error message

Already closed

What it means

BroadcastChannel.postMessage (ext/web/01_broadcast_channel.js:144) throws DOMException 'Already closed' with name InvalidStateError when the channel's closed flag is set. The flag is set by close() (and by the channel being removed), after which every postMessage is rejected before serialization. Once closed, the channel cannot be reopened.

Source

Thrown at ext/web/01_broadcast_channel.js:144

    ArrayPrototypePush(channels, this);
    refedBroadcastChannelsCount++;

    if (rid === null) {
      // Create the rid immediately, otherwise there is a time window (and a
      // race condition) where messages can get lost, because recv() is async.
      rid = op_broadcast_subscribe();
      recv();
    }
  }

  postMessage(message) {
    webidl.assertBranded(this, BroadcastChannelPrototype);

    const prefix = "Failed to execute 'postMessage' on 'BroadcastChannel'";
    webidl.requiredArguments(arguments.length, 1, prefix);

    if (this[_closed]) {
      throw new DOMException("Already closed", "InvalidStateError");
    }

    if (typeof message === "function" || typeof message === "symbol") {
      throw new DOMException("Uncloneable value", "DataCloneError");
    }

    // Serialize the message, carrying any SharedArrayBuffer backing stores
    // out-of-band (referenced by `sabId`) so the message can be deserialized by
    // an arbitrary number of receivers. `sabId` is 0 when there are none.
    const { 0: data, 1: sabId } = op_broadcast_serialize(message, null);

    // Send to other listeners in this VM.
    dispatch(this, this[_name], data, sabId);

    // Send to listeners in other VMs. This must happen before returning from
    // postMessage(), otherwise close() immediately after postMessage() can
    // cancel the deferred send before other workers observe the message.
    op_broadcast_send(rid, this[_name], data, sabId);

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Check bc.closed before posting and skip or rethrow deliberately
  2. Centralize teardown: call close() only after all sends have finished (single shutdown function, await pending sends first)
  3. If a channel may have been closed, create a fresh BroadcastChannel(name) instead of reusing the instance

Example fix

// before
function shutdown() {
  bc.close();
}
setInterval(() => bc.postMessage(tick()), 1000); // fires after shutdown -> throws

// after
let running = true;
function shutdown() {
  running = false;
  bc.close();
}
setInterval(() => {
  if (!running || bc.closed) return;
  bc.postMessage(tick());
}, 1000);
Defensive patterns

Strategy: validation

Validate before calling

if (!bc || bc.closed) {
  return; // channel already torn down
}
bc.postMessage(message);

Type guard

function isOpen(bc: BroadcastChannel | null): bc is BroadcastChannel {
  return bc !== null && !bc.closed;
}

Try / catch

try {
  bc.postMessage(message);
} catch (e) {
  if (e instanceof DOMException && e.name === "InvalidStateError") {
    // channel closed during shutdown; drop or re-create the channel
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: bc.postMessage(msg) after bc.close(); a finally block that closes the channel before an async send completes; two modules sharing a channel name where one closes its instance during shutdown while the other keeps sending.

Common situations: Shutdown/teardown ordering bugs in servers and workers; event handlers racing a close triggered by 'unload' or error paths; wrapper classes that auto-close on first error and then attempt to flush queued messages through the same channel.

Related errors


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