skip to content

Passing a value to structuredClone() or postMessage() can fail with a DOMException named DataCloneError. Which kinds of values cause that, and which values are copied but come back changed?

level: middleimportance: should knowfreq 45%

answer

  1. data crosses, behaviour does not
  2. functions and DOM nodes are refused
  3. some values fail loudly, some quietly
  4. prototype and descriptors do not survive
  5. own enumerable string keys only

basics

~20 s

Functions, symbols, DOM nodes, Promises, WeakMap/WeakSet and proxies are not serializable and throw a DataCloneError. Values that do clone can still change: prototypes are dropped, getters are invoked and flattened to data, and non-enumerable and symbol-keyed properties are skipped.

solid answer

~50 s

The structured clone algorithm has a fixed cloneable set, and anything outside it throws a `DOMException` with `name === 'DataCloneError'` — the call fails entirely, it does not return a partial copy. The usual culprits are functions (including a method stored as an own property, or an event handler bound in a constructor), symbols used as values, DOM nodes and other non-serializable platform objects, `Promise`, `WeakMap`/`WeakSet`, and proxies. The subtler failures are the values that clone *successfully* but come back different: the prototype is not preserved, so a class instance becomes a plain object and `instanceof` fails; only own **enumerable string-keyed** properties are copied, so non-enumerable and symbol-keyed properties disappear; and a getter is called once during serialization and stored as an ordinary data property, so it stops being live. In current browsers (Chrome 98+, Firefox 94+, Safari 15.4+) `Error` objects are cloneable, but a custom `Error` subclass returns as a base `Error`.

code

javascript · 14 lines
javascript
const obj = { plain: 1 };
Object.defineProperty(obj, 'hidden', { value: 2, enumerable: false });
Object.defineProperty(obj, 'computed', { get: () => 42, enumerable: true });
obj[Symbol('meta')] = 'dropped';

const clone = structuredClone(obj);
console.log(clone.plain, clone.hidden, clone.computed, Object.getOwnPropertySymbols(clone).length);
// 1 undefined 42 0

try {
  structuredClone({ onDone: () => {} });
} catch (err) {
  console.log(err.name); // "DataCloneError"
}

go deeper

for a junior

Recall that functions and DOM nodes cannot be cloned and that the attempt throws rather than skipping them. Be able to read a DataCloneError and guess which property caused it.

for a middle

Enumerate the non-serializable kinds and explain the quiet losses too — prototype, non-enumerable and symbol-keyed properties, and getters flattened into data. Explain why a symbol value throws while a symbol key is merely dropped.

for a senior

Show a debugging method for locating the offending node in a large graph, and argue for prevention: an explicit plain-data payload at every boundary rather than passing live domain objects and hoping.

for a principal

Frame the rule behind the rules — the algorithm moves data, never behaviour or identity, because the receiver shares no heap and no code. Use that to set a house convention for messages, stored records and history state.

## The two failure modes When a value crosses a structured-clone boundary — `structuredClone()`, `postMessage`, an IndexedDB write, `history.pushState` — one of three things happens. It clones faithfully, it clones but loses something, or it throws. Knowing which bucket a value falls into is the whole skill here, because the second bucket is where the production bugs live. ## Bucket one: throws DataCloneError The algorithm's serialize step raises a `DOMException` whose `name` is `"DataCloneError"` for anything it does not recognise. The call fails as a unit — there is no half-written clone and no partial message delivered. - **Functions.** Any function reachable from the graph, at any depth. This is the most common cause in real code, and it is rarely a top-level function: it is `this.onDone = () => {...}` assigned in a constructor, or a `toString` override attached as an own property. - **Symbols as values.** `{ tag: Symbol('x') }` throws. Note the asymmetry with symbol *keys* below. - **DOM nodes.** `Element`, `Node`, `Document` — none are serializable. Passing an element to a worker is the classic first attempt and the classic first `DataCloneError`. - **`Promise`, `WeakMap`, `WeakSet`.** Their contents cannot be meaningfully reconstructed elsewhere. - **Proxies.** A proxy has no internal slots the algorithm can inspect, so it throws even when the target is a plain object. - **Detached `ArrayBuffer`s.** A buffer already transferred away is unusable and throws. ```js try { structuredClone({ onDone: () => {} }); } catch (err) { console.log(err instanceof DOMException, err.name); // true "DataCloneError" } ``` A related trap: because the algorithm reads properties, a getter that throws will propagate *its own* exception out of the clone call, which is not a `DataCloneError` at all. Check `err.name` rather than assuming. ## Bucket two: clones, but changed These are silent and therefore worse. **Prototypes are not preserved.** The deserialize step builds ordinary objects. A `class User` instance arrives with `Object.prototype` as its prototype, so `clone instanceof User` is `false` and every prototype method is missing. The same applies to a custom `Error` subclass: `Error` and the standard subclasses are cloneable, carrying `name`, `message` and `cause`, but `class HttpError extends Error` comes back as a plain `Error`. The `stack` property is implementation-defined and should not be relied on. **Only own enumerable string-keyed properties survive.** The algorithm enumerates own enumerable string keys and copies those. So a property defined with `enumerable: false` is dropped, and a symbol-keyed property is dropped — quietly, unlike a symbol *value*, which throws. **Accessors are flattened.** A getter is invoked exactly once during serialization and its returned value is stored as a plain data property. The clone therefore holds a snapshot: a getter that computed `Date.now()` now holds a fixed number, and a setter is gone entirely. Any lazily-computed or derived state stops being derived. ```js const obj = { plain: 1 }; Object.defineProperty(obj, 'hidden', { value: 2, enumerable: false }); Object.defineProperty(obj, 'computed', { get: () => 42, enumerable: true }); obj[Symbol('meta')] = 'gone'; const c = structuredClone(obj); console.log(c.hidden, c.computed, Object.getOwnPropertySymbols(c).length); // undefined 42 0 ``` **A `RegExp` keeps source and flags but not `lastIndex`.** Fine for most uses, surprising for a stateful global regex. ## How to debug one in practice Browsers name the offending value in the exception message, but not always its path in the graph. The reliable technique is to reduce: clone the suspect object's branches one at a time, or write a small walker that tries `structuredClone` on each leaf and reports the first path that throws. In the long run, prevention beats debugging — define an explicit plain-data shape for anything that crosses a boundary and build it deliberately rather than handing over whatever object you happen to be holding. ## Why the design is this way The rules are not arbitrary. The receiving side may be a different thread, a different document, or a database file read back tomorrow — there is no shared heap and no shared code. A function's closure cannot be reconstructed there, a DOM node belongs to one document's tree, and a class's prototype only exists if the receiver happens to have loaded the same class definition. The algorithm copies *data*, never *behaviour or identity*, and every rule above follows from that single line.

  • Why does a symbol used as a property value throw, while a symbol used as a property key does not?
    Different steps of the algorithm. Enumerating an object's properties only visits own enumerable *string* keys, so a symbol-keyed property is never reached and is dropped silently. A symbol *value*, by contrast, is reached and handed to the serializer, which has no representation for it and throws DataCloneError.
  • How would you find which nested property is causing a DataCloneError in a large object?
    Bisect the graph: recursively walk the object and call `structuredClone` on each branch, recording the path of the first call that throws. That gives you the exact property path. Longer term, build an explicit plain-data payload for the boundary instead of cloning whatever object is at hand.
  • Are Error objects cloneable, and what arrives on the other side?
    Yes — Error and the standard subclasses are cloneable, carrying `name`, `message` and `cause`. But a custom subclass loses its prototype and returns as a base Error, and `stack` is implementation-defined. If the receiver needs to branch on the error kind, send a plain object with an explicit code field.

saying these in an interview costs you the question

  • Thinks unsupported values are dropped rather than throwing
  • Believes the failure yields a partial clone
  • Says class methods survive because the object cloned fine
  • Confuses a getter with a live accessor on the clone
  • Assumes DOM nodes can be posted to a worker

context