denoland/deno · error · DOMException

DataCloneError

DataCloneError

Error message

Cannot clone object of unsupported type.

What it means

On MessagePort.postMessage's fast path (no transfer list, at most one argument), the top-level message is checked with the internal `isUncloneable` helper (ext/web/13_message_port.js:308-316). Values whose prototype was marked not-serializable (Web API platform types such as URL, Headers, Request) or that were flagged per-instance via node:worker_threads `markAsUncloneable()` throw DataCloneError "Cannot clone object of unsupported type." before V8's serializer runs — without this check they would silently clone as `{}`.

Source

Thrown at ext/web/13_message_port.js:310

   * @param {object[] | StructuredSerializeOptions} transferOrOptions
   */
  postMessage(message, transferOrOptions = { __proto__: null }) {
    webidl.assertBranded(this, MessagePortPrototype);
    const prefix = "Failed to execute 'postMessage' on 'MessagePort'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    const portId = getMessagePortId(this);
    const portClosed = portId === null;
    // Fast path: no transferables - serialize and send in one shot,
    // bypassing the JsMessageData serde overhead
    if (
      transferOrOptions === undefined ||
      transferOrOptions === null ||
      (arguments.length <= 1)
    ) {
      if (portClosed) return;
      // Honor markAsUncloneable for top-level postMessage values.
      if (isUncloneable(message)) {
        throw new DOMException(
          "Cannot clone object of unsupported type.",
          "DataCloneError",
        );
      }
      const data = serializeMessageData(message, serializeErrorCb);
      const currentPortId = getMessagePortId(this);
      if (currentPortId === null) return;
      op_message_port_post_message_raw(currentPortId, data);
      return;
    }
    message = webidl.converters.any(message);
    let options;
    if (
      webidl.type(transferOrOptions) === "Object" &&
      transferOrOptions !== undefined &&
      transferOrOptions[SymbolIterator] !== undefined
    ) {
      const transfer = webidl.converters["sequence<object>"](

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Post plain serializable data: convert first (url.href, headers-to-array, JSON string).
  2. If the value is genuinely transferable (ArrayBuffer, another MessagePort), list it in the transfer list instead of relying on cloning.
  3. Run a `structuredClone` probe on unknown payloads before posting to fail early with a clear context.

Example fix

// before
port.postMessage(new URL("https://example.com"));

// after
port.postMessage(new URL("https://example.com").href);
Defensive patterns

Strategy: try-catch

Validate before calling

function isCloneableTopLevel(v) {
  if (v === null) return true;
  const t = typeof v;
  if (t !== "object" && t !== "function") return true; // primitives always clone
  try {
    structuredClone(v);
    return true;
  } catch {
    return false;
  }
}
if (!isCloneableTopLevel(msg)) msg = toPlainData(msg);

Type guard

const isPlainMessage = (v) => v === null || ["string", "number", "boolean", "bigint", "undefined"].includes(typeof v);

Try / catch

try {
  port.postMessage(msg);
} catch (e) {
  if (e instanceof DOMException && e.name === "DataCloneError") {
    port.postMessage(toPlainData(msg)); // strip platform objects, retry
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: `port.postMessage(new URL('https://example.com'))` with no second argument; posting a Request/Headers instance or a markAsUncloneable-flagged object as the entire message.

Common situations: Sending live web-platform objects between workers instead of plain data; Node-compat worker_threads code relying on markAsUncloneable semantics; refactors that forward framework objects through postMessage.

Related errors


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