Why structuredClone Fails Silently on Functions, DOM Nodes, and Symbols
You reach for structuredClone() to deep-copy a complex object, everything looks fine, and then a function that was definitely there is suddenly undefined. No error. No warning. Just gone. This is the kind of silent data loss that costs an hour of debugging before you even know where to look.
The good news: the behavior is fully specified. Once you know the rules, you can predict exactly what will and won't survive a clone β and you can pick the right tool for every situation.
What you'll learn
- Which value types
structuredClone()silently drops versus which ones throw an error - Why functions, Symbol-keyed properties, and DOM nodes each fail in a different way
- How the Structured Clone Algorithm actually works under the hood
- Drop-in alternatives for each failure category
- A quick decision table to pick the right cloning strategy
Prerequisites
You should be comfortable with plain JavaScript objects, know what a deep copy means in contrast to a shallow copy, and have a passing familiarity with Symbol and the DOM API. No frameworks required.
The Structured Clone Algorithm in Plain Terms
The browser (and Node.js) define a process called the Structured Clone Algorithm for serializing JavaScript values. It was originally designed for postMessage() β the mechanism that copies data across web workers and browser tabs. structuredClone(), added to the global scope in 2022, is simply a direct JavaScript binding to that same algorithm.
Because it was designed for safe cross-context message passing, it only supports a well-defined set of types: plain objects, arrays, primitives, Date, RegExp, Map, Set, ArrayBuffer, Blob, typed arrays, and a handful of others. Anything outside that list either gets silently omitted or causes a DataCloneError to be thrown. There is no plugin system and no custom serializer hook.
How Functions Disappear Without a Trace
This is the most dangerous failure mode because it is completely silent. When structuredClone() encounters a function as a property value, it does not throw β it simply omits the property from the cloned result.
const original = {
name: "parser",
parse: (str) => JSON.parse(str),
version: 3,
};
const copy = structuredClone(original);
console.log(copy.name); // "parser"
console.log(copy.version); // 3
console.log(copy.parse); // undefined <-- gone
The spec says function objects are not serializable under the algorithm. The rationale is sensible: a function contains executable code and a closure over its lexical scope. There is no safe, general way to serialize that across process or origin boundaries. But the silent omission is a real footgun when you're not expecting it.
If your object carries methods alongside data β which is common with class instances β every method will vanish. The cloned object will have the right data shape but will fail at runtime the moment you try to call anything on it.
What to do instead
If you need to deep-copy an object that contains functions, structuredClone() is the wrong tool. Your options are:
- Restructure the data: Separate the data properties from the methods. Clone the data, re-attach methods from the original class or a factory function.
- Write a targeted clone utility: If the object structure is known and stable, a hand-written recursive clone gives you full control.
- Use a library like Lodash's
_.cloneDeep(): It copies function references (not deep-copies them, since that's impossible) rather than dropping them silently.
import cloneDeep from "lodash/cloneDeep";
const copy = cloneDeep(original);
console.log(typeof copy.parse); // "function" <-- reference preserved
Keep in mind that Lodash copies the function reference, not a new independent function. Both objects share the same function. That's usually fine β and it's far better than silent deletion.
Why DOM Nodes Throw Instead of Clone
Unlike functions, DOM nodes do not get silently dropped. They cause structuredClone() to throw a DataCloneError immediately:
const node = document.querySelector("#app");
try {
const copy = structuredClone({ el: node, id: 42 });
} catch (err) {
console.log(err.name); // "DataCloneError"
console.log(err.message); // Failed to execute 'structuredClone' ...
}
A DOM node is a live object with references to its parent, children, event listeners, and the document it belongs to. There is no meaningful way to serialize that web of references into a portable byte stream and reconstitute it in another context. The spec mandates a hard error rather than a lossy copy.
What to do instead
If you need a copy of a DOM node, use the dedicated DOM API:
// Shallow clone β copies the node but not its descendants
const shallowCopy = node.cloneNode(false);
// Deep clone β copies the node and its entire subtree
const deepCopy = node.cloneNode(true);
Note that cloneNode(true) does not copy event listeners attached via addEventListener. Those need to be re-attached manually. Inline handlers set via onclick attributes are copied, but that's rarely how modern code works.
If your goal is to store a reference to a DOM node alongside serializable data, keep them separate. Serialize the data with structuredClone() and keep the DOM reference in a plain variable.
Symbol Keys Are Silently Ignored
Symbol-keyed properties on an object are not cloned. They are not thrown on β they are simply absent from the result, similar to what happens with functions.
const ID = Symbol("id");
const STATUS = Symbol("status");
const record = {
name: "invoice",
[ID]: 1001,
[STATUS]: "pending",
};
const copy = structuredClone(record);
console.log(copy.name); // "invoice"
console.log(copy[ID]); // undefined <-- dropped
console.log(copy[STATUS]); // undefined <-- dropped
This one bites library authors and anyone using Symbols as private-ish keys for metadata. The object looks correct on the surface, but any logic that depends on Symbol-keyed data will silently receive undefined.
What to do instead
If you control the data structure, the cleanest fix is to move Symbol-keyed values to string-keyed properties using a naming convention (e.g. __id, __status). That makes them visible to structuredClone() and to any serializer.
If you cannot change the structure, write a recursive clone that explicitly handles Object.getOwnPropertySymbols():
function deepCloneWithSymbols(value) {
if (value === null || typeof value !== "object") return value;
const clone = Array.isArray(value) ? [] : {};
// Copy string-keyed properties
for (const key of Object.keys(value)) {
clone[key] = deepCloneWithSymbols(value[key]);
}
// Copy Symbol-keyed properties
for (const sym of Object.getOwnPropertySymbols(value)) {
clone[sym] = deepCloneWithSymbols(value[sym]);
}
return clone;
}
const copy = deepCloneWithSymbols(record);
console.log(copy[ID]); // 1001
console.log(copy[STATUS]); // "pending"
This utility is intentionally simple. You'd extend it to handle Date, Map, Set, and other non-plain-object types based on what you actually need.
Other Types Worth Knowing About
Functions, DOM nodes, and Symbols are the most common surprises, but there are others worth keeping in the back of your mind.
- Class instances: The prototype chain is not preserved. A cloned class instance becomes a plain object. Methods accessed via the prototype are gone from the copy because the copy has
Object.prototype, not your class's prototype. - WeakMap and WeakSet: These throw a
DataCloneError. Their entire design relies on garbage-collection semantics that cannot be serialized. - Error objects: Supported in modern environments, but the stack trace is not preserved β only
nameandmessage. - Getter and setter functions: Only the current value returned by the getter is cloned. The getter and setter themselves are dropped.
The Decision Table: Which Tool for Which Job
| What you're copying | Best tool | Notes |
|---|---|---|
| Plain data objects (no functions, no Symbols) | structuredClone() | Fast, built-in, handles circular refs |
| Objects with function references | _.cloneDeep() or manual | Functions are referenced, not deep-copied |
| Objects with Symbol-keyed properties | Custom recursive clone | Walk getOwnPropertySymbols() |
| DOM node subtrees | node.cloneNode(true) | Re-attach event listeners manually |
| Class instances (retain prototype) | Manual clone or _.cloneDeep() | structuredClone() loses the prototype |
| Shallow copy only | Object.assign() or spread | Fast, no deep traversal needed |
Common Pitfalls
Assuming silence means success. The fact that structuredClone() does not throw when it drops a function or a Symbol key is the core danger. Always validate the cloned result in tests, especially if the source object shape can change over time.
Cloning class instances expecting the prototype. If you clone a class instance and then call a method on the copy, you'll get a TypeError: copy.methodName is not a function. The data is there; the class is not. Serialize the data, not the instance.
Mixing serializable and non-serializable data in the same object. This is the architecture-level mistake. If you store a DOM node alongside form data in a single object, you can't clone it cleanly with any single tool. Split data from UI references at the design stage.
Forgetting structuredClone() handles circular references but most alternatives do not. If your data has circular references, _.cloneDeep() handles them, but a naive recursive clone will stack-overflow. structuredClone() is one of the cleanest built-in options for circular data graphs β as long as your types are all supported.
Wrapping Up
The Structured Clone Algorithm is a well-defined, reliable tool for copying plain serializable data. It fails predictably β but only if you know the rules in advance.
Here are your concrete next steps:
- Audit your clone sites. Search your codebase for
structuredClone(calls and check whether any of the objects being cloned contain functions, Symbol keys, class instances, or DOM references. - Add a test that validates the shape of cloned objects. A simple assertion that a method exists on the result will catch the silent-drop issue immediately.
- Use
_.cloneDeep()as the default when object shape is unpredictable and function references need to survive. Accept that functions are referenced, not duplicated. - Write a
deepCloneWithSymbolsutility if your codebase uses Symbols as metadata keys. Keep it in a shared utils module and extend it as needed. - Separate data from UI. Architectural decisions that keep DOM nodes and serializable state in different objects will save you from mixed-type clone failures entirely.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!