If you've ever written JSON.parse(JSON.stringify(obj)) to deep copy an object, you've probably been burned by it. It strips undefined, kills functions, turns Date objects into strings, and silently drops Map, Set, RegExp, and everything else JSON can't represent.
structuredClone() fixes all of that. It's a native browser API that performs a true deep clone, handling the types you actually use — without any libraries.
The Problem with Existing Approaches
Before structuredClone(), deep copying in JavaScript was a mess:
// The JSON hack — everyone's guilty of this
const original = { date: new Date(), fn: () => {}, undef: undefined };
const copy = JSON.parse(JSON.stringify(original));
// copy.date → "2026-04-13T..." (string, not Date)
// copy.fn → gone
// copy.undef → gone// Spread / Object.assign — shallow only
const original = { nested: { a: 1 } };
const copy = { ...original };
copy.nested.a = 99;
original.nested.a; // 99 — mutated the original!// Libraries (lodash.cloneDeep) — works, but adds a dependency
import { cloneDeep } from 'lodash-es';
const copy = cloneDeep(original);None of these are great. The JSON hack silently loses data. Spread is shallow. Libraries add weight. structuredClone() is the native answer.
How structuredClone() Works
const original = {
name: "JavaScriptBit",
tags: new Set(["js", "frontend"]),
data: new Map([["key", { value: 42 } }]),
created: new Date(),
buffer: new ArrayBuffer(8),
regex: /pattern/gi,
extra: undefined,
};
const clone = structuredClone(original);
// Everything is preserved and independent
clone !== original; // true
clone.tags !== original.tags; // true
clone.data !== original.data; // true
clone.created instanceof Date; // true
clone.buffer instanceof ArrayBuffer; // true
clone.regex.source === original.regex.source; // true
clone.extra === undefined; // true
// Mutating the clone doesn't affect the original
clone.tags.add("new-tag");
original.tags.has("new-tag"); // falseOne function. No options. No gotchas. It just works.
Supported Types
structuredClone() handles the full structured clone algorithm:
| Type | Supported | Notes |
|---|---|---|
| Plain objects & arrays | ✅ | Deep cloned recursively |
Date | ✅ | Preserved as Date instances |
RegExp | ✅ | Flags preserved |
Map & Set | ✅ | Keys and values deep cloned |
ArrayBuffer | ✅ | New buffer with copied bytes |
TypedArray (Uint8Array, etc.) | ✅ | New buffer, same type |
DataView | ✅ | Backed by new ArrayBuffer |
Blob | ✅ | New Blob, same data |
File | ✅ | Name and metadata preserved |
Error | ✅ | Message and type preserved |
undefined | ✅ | Not stripped (unlike JSON) |
Infinity, NaN | ✅ | Preserved |
ImageData | ✅ | Pixel data deep cloned |
| Functions | ❌ | Throws DataCloneError |
| DOM nodes | ❌ | Throws DataCloneError |
| Symbols | ❌ | Throws DataCloneError |
| WeakMap / WeakSet | ❌ | Throws DataCloneError |
Real-World Patterns
Immutable State Updates (Redux-style)
function reducer(state, action) {
// Before: spread was shallow, missed nested changes
// After: true deep clone for safe immutable updates
const draft = structuredClone(state);
switch (action.type) {
case "UPDATE_USER":
draft.user.profile.name = action.payload;
return draft;
case "ADD_TAG":
draft.tags.add(action.payload);
return draft;
default:
return state;
}
}Cloning Before Mutation
async function updateConfig(serverConfig) {
// Clone so we don't mutate the cached version
const localConfig = structuredClone(serverConfig);
localConfig.lastUpdated = new Date();
localConfig.overrides.set("timeout", 5000);
await fetch("/api/config", {
method: "PUT",
body: JSON.stringify(localConfig),
});
}Sending Complex Data to a Web Worker
// structuredClone is what postMessage uses internally
// But you can pre-clone if you need the data in both contexts
const dataset = structuredClone(largeDataset);
worker.postMessage({ type: "PROCESS", data: dataset });
// You still have the original — clone is independent
console.log(largeDataset.records.length); // still worksSnapshot Pattern for Undo/Redo
const history = [];
function saveState(state) {
history.push(structuredClone(state));
}
function undo() {
if (history.length > 1) {
history.pop(); // discard current
return structuredClone(history[history.length - 1]);
}
}
// Usage
const canvas = { layers: new Map(), background: null };
saveState(canvas);
canvas.layers.set("text", { content: "Hello", position: { x: 100, y: 50 } });
saveState(canvas);
canvas.layers.set("text", { content: "Hello, World!", position: { x: 100, y: 50 } });
const previous = undo();
// previous.layers.get("text").content === "Hello"Performance: structuredClone vs Alternatives
const largeObject = Array.from({ length: 100_000 }, (_, i) => ({
id: i,
data: new Uint8Array(100),
meta: { created: new Date(), tags: new Set([`tag-${i}`]) },
}));
console.time("JSON parse/stringify");
JSON.parse(JSON.stringify(largeObject));
console.timeEnd("JSON parse/stringify");
// Note: loses Uint8Array, Date, Set — but this is what most people use
console.time("structuredClone");
structuredClone(largeObject);
console.timeEnd("structuredClone");Results vary by engine, but structuredClone() is typically faster than JSON round-trip for complex types because it doesn't need to serialize to a string intermediate. More importantly, it's correct — you get back what you put in.
Common Pitfalls
Functions Throw
const obj = { callback: () => console.log("hi") };
structuredClone(obj); // ❌ DataCloneErrorIf you need to clone objects with functions, you'll still need a library like Lodash or a custom recursive cloner.
Circular References Are Handled
const obj = { name: "test" };
obj.self = obj;
const clone = structuredClone(obj);
clone !== obj; // true
clone.self === clone; // true — circular ref preserved correctlyThis is something JSON.parse(JSON.stringify()) completely fails on (throws TypeError: Converting circular structure to JSON).
Property Descriptors Are Not Preserved
const obj = {};
Object.defineProperty(obj, "hidden", {
value: 42,
enumerable: false,
writable: false,
});
const clone = structuredClone(obj);
Object.getOwnPropertyDescriptor(clone, "hidden");
// { value: 42, writable: true, enumerable: true, configurable: true }The value survives, but the descriptor resets to defaults. If you need getter/setter or non-enumerable properties preserved, structuredClone() won't do it.
Prototype Chain Is Lost
class User {
constructor(name) { this.name = name; }
greet() { return `Hi, I'm ${this.name}`; }
}
const user = new User("Divyam");
const clone = structuredClone(user);
clone instanceof User; // false
clone.greet; // undefined — method lost
clone.name; // "Divyam" — data preservedPlain data survives. The class hierarchy and methods don't. This is by design — structuredClone copies data, not behaviour.
Browser Support
structuredClone() is available in all modern browsers since early 2022:
- Chrome 98+
- Firefox 94+
- Safari 15.4+
- Node.js 17+
If you need to support older browsers, core-js provides a polyfill.
When to Use What
| Approach | Use When |
|---|---|
structuredClone() | Deep copying plain data objects with Maps, Sets, Dates, ArrayBuffers |
Spread ({...obj}) | Shallow clone is sufficient |
JSON.parse/JSONify | You only have JSON-safe data and need widest compat |
Lodash cloneDeep | Objects contain functions, class instances, or custom descriptors |
| Custom cloner | You need fine-grained control over what gets cloned |
Key Takeaways
structuredClone()is the native deep copy API — no libraries, no hacks- It handles
Date,Map,Set,RegExp,ArrayBuffer,TypedArray,Blob,File, circular references, andundefined - It cannot clone functions, DOM nodes, class methods, or property descriptors
- It's faster and more correct than
JSON.parse(JSON.stringify())for complex types - Available in all modern browsers and Node.js 17+
- If you're still using the JSON hack, switch today — your future self will thank you