structuredClone(): The Deep Copy That Isn't Always Deep
structuredClone() handles circular refs and built-in types, but silently drops prototypes, class instances, getters, and Symbol properties.
structuredClone() is the first native deep-copy primitive JavaScript has ever had.
It handles circular references, Date, Map, Set, RegExp,
ArrayBuffer, and typed arrays — things JSON.parse(JSON.stringify()) chokes on.
But it's not the universal clone you think it is. Here's what it silently drops, what it throws on, and when
you still need a custom solution.
What It Can't Clone (Throws)
structuredClone() uses the
structured clone algorithm,
originally designed for postMessage() between workers. Anything that can't cross a worker boundary can't be cloned:
// ❌ Functions — DataCloneError
structuredClone({ handler: () => {} });
// ❌ DOM nodes — DataCloneError
structuredClone({ el: document.body });
// ❌ Symbols as values — DataCloneError
structuredClone({ id: Symbol('unique') });
These aren't silent failures — they throw DOMException: DataCloneError.
But the error only fires at runtime, and if the non-cloneable value is deeply nested,
debugging which property caused it is painful. There's no path information in the error message.
What It Silently Drops
More dangerous than the throws are the silent losses:
class User {
constructor(name) { this.name = name; }
greet() { return `Hi, ${this.name}`; }
}
const original = new User('Alice');
const clone = structuredClone(original);
clone instanceof User; // ❌ false
clone.greet(); // ❌ TypeError: clone.greet is not a function
Object.getPrototypeOf(clone) === Object.prototype; // ✅ true
The prototype chain is gone. The clone is a plain Object with the instance's
own enumerable properties. Methods defined on the prototype, Symbol properties,
getters/setters, and property descriptors (writable, enumerable, configurable) are all lost.
The Shared Reference Gotcha
structuredClone() preserves structural sharing within the object graph.
This is actually correct behavior but surprises developers who expect fully independent nested copies:
const shared = ['a', 'b'];
const obj = { x: shared, y: shared };
const clone = structuredClone(obj);
clone.x === clone.y; // ✅ true — same reference in clone
clone.x === obj.x; // ❌ false — different from original
clone.x.push('c');
console.log(clone.y); // ['a', 'b', 'c'] — y reflects x's changeThe clone faithfully reproduces the original's reference topology. If two properties pointed to the same array in the original, they point to the same (cloned) array in the copy. This is by design — it's how circular references work — but it means mutations within the clone can cascade in unexpected ways.
Performance: It's Not Free
structuredClone() is significantly faster than JSON.parse(JSON.stringify())
for objects with Date, Map, or Set — because JSON has to
serialize and reparse. But for plain objects and arrays (the most common case), JSON roundtrip
is often faster in V8:
// Benchmark (plain objects, V8/Chrome 120+):
// JSON roundtrip: ~0.8ms for 10K entries
// structuredClone: ~1.2ms for 10K entries
// Spread (shallow): ~0.1ms for 10K entries
// For objects with Maps/Sets/Dates:
// JSON roundtrip: Incorrect output (loses types)
// structuredClone: ~1.0ms and correct
Rule of thumb: if your data is JSON-safe (no Date, Map, Set,
circular refs, or typed arrays) and performance matters, JSON roundtrip may still be faster.
Profile before switching.
The Transfer Trick
structuredClone() accepts a transfer option that moves
ArrayBuffers instead of copying them. The original becomes zero-length — detached.
This is a zero-copy operation:
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
const clone = structuredClone(
{ data: buffer },
{ transfer: [buffer] }
);
buffer.byteLength; // 0 — original is detached
clone.data.byteLength; // 1048576 — moved, not copied
This is identical to how postMessage() transfers work.
Use it when passing large buffers between processing stages where you don't need the original.
But be careful: accessing the detached buffer throws TypeError.
When to Use What
structuredClone()— Default choice. Use for anything withDate,Map,Set, circular refs, or typed arraysJSON.parse(JSON.stringify())— JSON-safe data only. Faster for large plain objects in V8. Dropsundefined,Infinity,NaN, and functions silently- Spread /
Object.assign— Shallow copy only. Fine for flat config objects - Custom deep clone — When you need prototype preservation, function cloning, or property descriptor retention
Error Handling Pattern
Since structuredClone() throws on non-cloneable values, wrap it when the input shape isn't guaranteed:
function safeClone(obj) {
try {
return structuredClone(obj);
} catch (e) {
if (e instanceof DOMException && e.name === 'DataCloneError') {
// Fallback: strip non-cloneable values
return JSON.parse(JSON.stringify(obj));
}
throw e;
}
}This pattern is especially useful in state management libraries where user-provided state might contain functions or DOM refs you can't predict.
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement