Skip to main content

Cloudsoft Zone Build, deploy and scale with practical guides to PHP, Node.js, React, DevOps and AI automation. Explore the guides

Cloudsoft Zone
JavaScriptComparison

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.

Cloudsoft Zone

6 min read

Xerox AltaLink C8045 office photocopier with a touch screen control panel and paper trays
Photograph: Baron Maddock / CC BY 4.0, via Wikimedia Commons
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

ApproachDepthKeeps Date, Map, SetCircular referencesClass instancesFunctions
{ ...obj }, Object.assign(), [...arr]One levelShares the originalsShares themBecomes a plain objectCopies the reference
JSON.parse(JSON.stringify(obj))DeepNo (string, {}, {})Throws TypeErrorPlain object, no methodsSilently dropped
structuredClone(obj)DeepYesPreservedPlain object, no methodsThrows DataCloneError
Your own clone() methodWhatever you writeIf you copy themIf you handle themYesYes

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:

JavaScript
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 reference

Spread: fast, but shallow

JavaScript
const shallow = { ...original };
shallow.steps[0].done = true;
console.log(original.steps[0].done); // true: the nested object is shared

Spread 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:

JavaScript
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 entirely

None 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.

Sheet of blue carbon paper lifted from a page to reveal the handwritten word karbonpapper copied beneath
Like a carbon copy that loses colour, a JSON round trip keeps the outline of your data but drops dates, Maps and Sets.

structuredClone: deep and faithful

JavaScript
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]; // true

Every 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 a DataCloneError (a DOMException). 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 instanceof checks do not.
  • Private fields are dropped. #secret class 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 symbol values cannot be cloned.
  • RegExp loses lastIndex.
JavaScript
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:

JavaScript
const bytes = new Uint8Array(1024 * 1024);
const moved = structuredClone(bytes, { transfer: [bytes.buffer] });

bytes.byteLength; // 0: the original buffer is detached
moved.byteLength; // 1048576
Line drawing of a flatbed photocopier with its lid open and paper trays on both sides
structuredClone copies data faithfully, but behaviour such as methods and getters stays with the original class.

Copying class instances properly

When an object has behaviour, give the class its own copy method and use structuredClone() for the data inside it:

JavaScript
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 with structuredClone().
  • 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 a clone() 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() calls Date.prototype.toJSON(), which returns an ISO string, and JSON.parse() has no way to know the string was a date. It also drops undefined and functions, turns NaN into null and empties Map and Set.

  • 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.

Advertise on CloudSoftZone

Your next customer could start right here.

Introduce your tool, product or business to readers exploring technology.

Advertise with us

Explore placements and request a private quote

Sources

  1. Window: structuredClone() method — MDN Web Docs
  2. The structured clone algorithm — MDN Web Docs
  3. JSON.stringify() — MDN Web Docs
  4. Spread syntax (...) — MDN Web Docs
  5. 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.

Never published. Used only to contact you about this comment.

Loading the security check…

Comments are moderated before publication.