denoland/deno · error · DOMException

DataCloneError

DataCloneError

Error message

Uncloneable value

What it means

BroadcastChannel.postMessage (ext/web/01_broadcast_channel.js:148) rejects functions and symbols with DOMException DataCloneError before serialization, because structured clone (op_broadcast_serialize) can never clone them. Values nested deeper in the object graph are caught by the serializer itself, but the top-level typeof check short-circuits the common cases with this exact message.

Source

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

      // 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);

    // In-VM dispatch deserialized eagerly and op_broadcast_send moved a clone
    // of the backing stores into the cross-VM message, so the sender's stash
    // entry is no longer needed.

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Send data only: strip functions and symbols before posting
  2. Represent behavior as a serializable descriptor (e.g. { type: 'run', id, args }) and dispatch on the receiving side
  3. Replace symbol keys with string keys for anything that crosses the channel

Example fix

// before
bc.postMessage({ run: () => doWork(), id: Symbol('job') });

// after
bc.postMessage({ type: 'run', jobId: 'job-42', args: [1, 2] });
Defensive patterns

Strategy: type-guard

Validate before calling

if (typeof message === "function" || typeof message === "symbol") {
  throw new TypeError("BroadcastChannel payloads must be data");
}
bc.postMessage(message);

Type guard

function isCloneableTopLevel(v: unknown): boolean {
  return typeof v !== "function" && typeof v !== "symbol";
}

Try / catch

try {
  bc.postMessage(message);
} catch (e) {
  if (e instanceof DOMException && e.name === "DataCloneError") {
    bc.postMessage(JSON.parse(JSON.stringify(message, (_k, val) =>
      typeof val === "function" || typeof val === "symbol" ? undefined : val,
    )));
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: bc.postMessage(() => {}) or passing a handler reference instead of invoking it; bc.postMessage(Symbol('payload')); objects whose graph contains function or symbol values deeper inside.

Common situations: Trying to ship callbacks across tabs/workers as if BroadcastChannel were RPC; passing a method reference with missing parentheses; action objects keyed by symbol ids from stores or registries; sending class instances that carry symbol-valued fields.

Related errors


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