structuredClone vs JSON vs spread: deep copy JavaScript safely
Spread is shallow, JSON loses dates, Maps, Sets and undefined, and structuredClone keeps them. A tested comparison of JavaScript copy methods and their limits.
6 min read

In this article
To deep-copy data in JavaScript today, use structuredClone(). It copies nested objects, arrays, Date, Map, Set, typed arrays, BigInt values and even circular references, and it is built into every current browser. The spread operator only copies one level, and JSON.parse(JSON.stringify()) silently turns dates into strings and drops Map, Set and undefined. What none of them do is copy class instances with their methods; for that you still need your own clone() method.
This comparison shows exactly what each approach keeps and loses, using one test object run through all three on Node.js 24, plus the edge cases from MDN's description of the structured clone algorithm.
The quick answer
| Approach | Depth | Keeps Date, Map, Set | Circular references | Class instances | Functions |
|---|---|---|---|---|---|
{ ...obj }, Object.assign(), [...arr] | One level | Shares the originals | Shares them | Becomes a plain object | Copies the reference |
JSON.parse(JSON.stringify(obj)) | Deep | No (string, {}, {}) | Throws TypeError | Plain object, no methods | Silently dropped |
structuredClone(obj) | Deep | Yes | Preserved | Plain object, no methods | Throws DataCloneError |
Your own clone() method | Whatever you write | If you copy them | If you handle them | Yes | Yes |
structuredClone() is available in Chrome and Edge 98, Firefox 94, Safari 15.4 and Node.js 17 and later, according to MDN's compatibility data (version 8.1.5). That covers every browser that has received updates in the past few years.
One test object, three copies
Here is a realistic piece of application state, the kind you might copy before editing it in a form:
const original = {
title: 'Launch plan',
due: new Date('2026-11-02T09:30:00Z'),
tags: new Set(['q4', 'web']),
owners: new Map([['lead', { name: 'Asha' }]]),
budget: 12000n,
notes: undefined,
score: NaN,
steps: [{ done: false }],
};
original.self = original; // a circular referenceSpread: fast, but shallow
const shallow = { ...original };
shallow.steps[0].done = true;
console.log(original.steps[0].done); // true: the nested object is sharedSpread copies the top-level properties and nothing else. Every nested object, array, Date, Map and Set is the same object in both copies. That is often exactly what you want for immutable updates in React or Redux-style code, where you replace the parts you change: { ...state, steps: state.steps.map(...) }. It is a bug when you meant to make an independent copy.
JSON round trip: deep, but lossy
With the object above, JSON.stringify() throws straight away: first on the BigInt ("Do not know how to serialize a BigInt"), and, once that is removed, on the circular reference. Removing both and trying again shows what is quietly lost:
const { self, budget, ...rest } = original;
const viaJson = JSON.parse(JSON.stringify(rest));
// {
// title: 'Launch plan',
// due: '2026-11-02T09:30:00.000Z', // a string, not a Date
// tags: {}, // Set contents lost
// owners: {}, // Map contents lost
// score: null, // NaN became null
// steps: [ { done: false } ]
// } // "notes" is gone entirelyNone of these losses raise an error. They show up later as due.getTime is not a function or as a tag list that is mysteriously empty. JSON is the right tool for serialising data to send or store, not for copying it in memory.

structuredClone: deep and faithful
const copy = structuredClone(original);
copy.due instanceof Date; // true
copy.tags instanceof Set; // true
copy.owners.get('lead'); // { name: 'Asha' } (a new object)
copy.budget; // 12000n
'notes' in copy; // true
Number.isNaN(copy.score); // true
copy.self === copy; // true: the cycle points at the copy
copy.steps[0] !== original.steps[0]; // trueEvery value came through with its type intact, and the circular reference now points at the new object rather than the old one. MDN explains why: the algorithm keeps a map of objects it has already visited, so it copies each one once and reuses it.
What structuredClone will not copy
The structured clone algorithm is the same one browsers use for postMessage() to workers and for IndexedDB, so it is designed for data, not behaviour. MDN lists the limits:
- Functions throw.
structuredClone({ onSave() {} })throws aDataCloneError(aDOMException). DOM nodes throw too. - Prototypes are not copied. A class instance comes back as a plain object: its own data properties survive, but its methods, getters and
instanceofchecks do not. - Private fields are dropped.
#secretclass fields are not duplicated. - Getters become values; descriptors are reset. A getter is called once and its result stored as a normal writable property. A frozen object's copy is not frozen.
- Symbol-keyed properties are not copied, and
symbolvalues cannot be cloned. RegExploseslastIndex.
class Task {
#secret = 1;
constructor(title) { this.title = title; }
get upper() { return this.title.toUpperCase(); }
}
const t = structuredClone(new Task('ship'));
t instanceof Task; // false
t.upper; // undefined
t; // { title: 'ship' }Errors clone well
Error objects are supported. In our test, a TypeError with a cause came back as a TypeError with the same message, its cause and a stack. MDN notes that browsers must keep name and message and are expected to keep stack and cause, so do not rely on the last two everywhere.
Bonus: transfer instead of copy
For large binary data, the transfer option moves an ArrayBuffer into the result instead of copying it. The original becomes unusable, which is also a guarantee that nobody modifies it afterwards:
const bytes = new Uint8Array(1024 * 1024);
const moved = structuredClone(bytes, { transfer: [bytes.buffer] });
bytes.byteLength; // 0: the original buffer is detached
moved.byteLength; // 1048576
Copying class instances properly
When an object has behaviour, give the class its own copy method and use structuredClone() for the data inside it:
class Draft {
constructor({ title, due, tags }) {
this.title = title;
this.due = due;
this.tags = tags;
}
clone() {
return new Draft(structuredClone({ title: this.title, due: this.due, tags: this.tags }));
}
}This keeps the prototype, runs the constructor's validation, and still gets a faithful deep copy of the fields. Utility libraries such as Lodash also offer deep-clone helpers; check their documentation for how they treat class instances, functions and DOM nodes before swapping one for another.
Performance: measure your own data
For plain JSON-like data, the JSON round trip can be competitive. In one informal run on Node.js 24 with 20,000 small nested objects, the JSON round trip averaged about 11 ms and structuredClone() about 19 ms. That single measurement depends heavily on data shape and engine, so treat it as a reason to measure, not as a rule. Correctness usually matters more: a fast copy that turns dates into strings is not a copy. And if you clone large state often, the better optimisation is usually to clone less, for example by updating immutably with spread.
Gotchas
- Proxies and reactive state. State objects from some front-end frameworks are proxies or carry extra metadata, and may fail with
DataCloneError. Convert them to plain data with the framework's own helper first. - Event handlers in data. One stray callback anywhere in the tree makes the whole clone throw. Keep functions out of the data you copy.
- Do not use JSON for "deep equality" either. Key order and the losses above make
JSON.stringify(a) === JSON.stringify(b)unreliable.
What to do next
- Search your code base for
JSON.parse(JSON.stringify(and replace the ones used for copying withstructuredClone(). - Where you clone class instances, add a
clone()method instead of relying on a generic helper. - Use spread for immutable updates,
structuredClone()for independent copies, and JSON only for serialisation.
Frequently asked questions
What is the best way to deep copy an object in JavaScript?
For plain data,
structuredClone(). It handles nested objects,Date,Map,Set, typed arrays and circular references. For class instances, add aclone()method to the class.Is the spread operator a deep copy?
No. Spread copies only the top-level properties; nested objects and arrays are shared between the original and the copy.
Why does JSON.parse(JSON.stringify()) break dates?
JSON.stringify()callsDate.prototype.toJSON(), which returns an ISO string, andJSON.parse()has no way to know the string was a date. It also dropsundefinedand functions, turnsNaNintonulland emptiesMapandSet.Why does structuredClone throw DataCloneError?
Something in the value cannot be cloned, most often a function, a DOM node or a proxy. Remove it from the data or convert the value to plain data first.
Does structuredClone keep class methods?
No. The prototype chain is not copied, so a class instance becomes a plain object with its own data properties. Private fields are dropped as well.
Which browsers support structuredClone?
Chrome and Edge 98, Firefox 94, Safari 15.4 and later, and Node.js 17 and later, according to MDN's compatibility data.
Your next customer could start right here.
Introduce your tool, product or business to readers exploring technology.
Explore placements and request a private quote
Sources
- Window: structuredClone() method — MDN Web Docs
- The structured clone algorithm — MDN Web Docs
- JSON.stringify() — MDN Web Docs
- Spread syntax (...) — MDN Web Docs
- MDN browser-compat-data 8.1.5 (data.json) — MDN Web Docs, 2026-10-08
Written with substantial AI assistance. Facts checked against cited sources. Editorial policy.
Comments
No comments yet. Be the first to join the conversation.
Leave a comment
Every comment is read by a moderator before it appears. Your email address is never published; it is used only to contact you about this comment.
More from Cloudsoft Zone

TutorialJavaScript
JavaScript using declarations: automatic cleanup without finally
Learn JavaScript's using and await using declarations, Symbol.dispose and DisposableStack, with tested browser recipes and October 2026 support data.
By Cloudsoft Zone

Architecture guideJavaScript
Ship JavaScript without a bundler: import maps and JSON modules
How to structure a front end as native ES modules: import maps for bare imports, JSON modules, modulepreload against waterfalls, and cache-busting.
By Cloudsoft Zone

Troubleshooting guideJavaScript
Fix 'Uncaught (in promise)': find every unhandled rejection
Why browsers log Uncaught (in promise), the five code patterns behind most unhandled rejections, how to find them in DevTools, and a safe global handler.
By Cloudsoft Zone
