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?
answer
- data crosses, behaviour does not
- functions and DOM nodes are refused
- some values fail loudly, some quietly
- prototype and descriptors do not survive
- own enumerable string keys only
basics
~20 sFunctions, 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 sThe 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 linesconst 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
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.
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.
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.
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