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
- Send data only: strip functions and symbols before posting
- Represent behavior as a serializable descriptor (e.g. { type: 'run', id, args }) and dispatch on the receiving side
- 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
- Send plain data (message objects with type + args), never callbacks
- Check for missing parentheses when passing method results
- Avoid symbols anywhere in payloads that cross structured-clone boundaries
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
- InvalidStateError
- DataCloneError
- DataCloneError
- DataCloneError
- Unsupported KeyObject type for structured clone: ${data.keyT
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/a3f7ef3f4a82e01e.
Report an issue: GitHub.