structuredClone(): The Right Way to Deep Copy in JavaScript

structuredClone(): The Right Way to Deep Copy in JavaScript

The JSON Hack Is Over

For years, the standard way to deep copy an object in JavaScript was this:

const clone = JSON.parse(JSON.stringify(original));

It worked for simple objects, but it was a hack with real consequences: it silently drops undefined values, converts Date objects to strings, can’t handle Map, Set, RegExp, ArrayBuffer, or circular references, and throws on BigInt. If you’ve ever had a cloned object behave differently than the original and spent an hour debugging, the JSON roundtrip was probably the culprit.

structuredClone() is the native replacement. It’s built into every modern browser and Node.js, handles all the types JSON can’t, and doesn’t silently corrupt your data.

Basic Usage

The API is about as simple as it gets:

const original = {
  name: 'Alice',
  joined: new Date('2026-01-15'),
  roles: new Set(['admin', 'editor']),
  preferences: {
    theme: 'dark',
    notifications: true
  }
};

const clone = structuredClone(original);

// The clone is completely independent
clone.preferences.theme = 'light';
clone.roles.add('viewer');

console.log(original.preferences.theme); // 'dark' — unchanged
console.log(original.roles.has('viewer')); // false — unchanged

// Date objects are properly cloned (not stringified)
console.log(clone.joined instanceof Date); // true
console.log(clone.joined.getFullYear());  // 2026

Compare that to JSON.parse(JSON.stringify(original)), which would have turned joined into a string, completely lost the Set, and given you a plain array or nothing at all.

What structuredClone Handles

The algorithm used by structuredClone() is the same structured clone algorithm used by postMessage(), IndexedDB, and the History API. It handles a wide range of types:

TypestructuredCloneJSON roundtrip
Plain objects / arraysYesYes
DateYes (preserves type)Converts to string
Map / SetYesLost entirely
RegExpYesBecomes empty object {}
ArrayBuffer / TypedArrayYesLost
Blob / FileYesLost
BigIntYesThrows TypeError
Circular referencesYesThrows TypeError
undefined valuesPreservedSilently dropped
NaN / InfinityPreservedBecomes null

Circular References

One of the most common edge cases: objects that reference themselves. JSON.stringify() throws a TypeError. structuredClone() handles it correctly.

const obj = { name: 'root' };
obj.self = obj; // Circular reference

// JSON.stringify(obj); // Throws: TypeError: Converting circular structure

const cloned = structuredClone(obj);
console.log(cloned.self === cloned); // true — circular ref preserved
console.log(cloned === obj);         // false — different object

This comes up more often than you’d think — tree structures with parent references, linked lists, cached graph traversals. If your data structure has any circularity, structuredClone() is the only native option that works.

What structuredClone Cannot Clone

The structured clone algorithm has intentional limitations. It throws a DataCloneError for:

  • Functions — can’t be serialized. If your object has methods, they’ll cause an error.
  • DOM nodesElement, Document, etc. are not cloneable.
  • Symbols — Symbol-keyed properties are ignored.
  • Property descriptors — getters, setters, and non-enumerable properties are not preserved. Only the data values are copied.
  • Prototype chains — the clone is a plain object. Class instances lose their prototype and methods.
class User {
  constructor(name) { this.name = name; }
  greet() { return `Hi, I'm ${this.name}`; }
}

const user = new User('Alice');
const clone = structuredClone(user);

console.log(clone.name);  // 'Alice' — data is copied
console.log(clone.greet); // undefined — method is lost
console.log(clone instanceof User); // false

If you need to clone class instances with methods, you’ll need a custom clone method on your class or a library like Lodash’s cloneDeep.

The Transferable Trick

The second argument to structuredClone() lets you transfer objects instead of copying them. Transferred objects become unusable in the original context but avoid the cost of copying large binary data.

const buffer = new ArrayBuffer(1024 * 1024); // 1 MB

// Transfer instead of copy — zero-copy for large buffers
const clone = structuredClone(buffer, { transfer: [buffer] });

console.log(clone.byteLength);  // 1048576 — the clone has the data
console.log(buffer.byteLength); // 0 — original is neutered

This is mainly useful when working with large binary data (images, audio, video buffers) where copying would be expensive. In most application code, you won’t need it.

Performance: When to Use What

Here’s my recommendation based on what you’re actually copying:

ScenarioBest Approach
Shallow copy of a flat objectSpread: { ...obj }
Shallow copy of an array[...arr] or arr.slice()
Deep copy of JSON-safe datastructuredClone()
Deep copy with Dates, Maps, SetsstructuredClone()
Deep copy of class instancesCustom clone method or Lodash
Large binary data (ArrayBuffer)structuredClone() with transfer

For simple, flat objects, the spread operator is orders of magnitude faster than structuredClone(). Don’t use structuredClone() when a shallow copy is sufficient — it’s overkill.

For deeply nested objects, structuredClone() and JSON.parse(JSON.stringify()) perform similarly in benchmarks on JSON-safe data (JSON is sometimes faster for very simple objects). But structuredClone() wins on correctness and type preservation every time. In production, correctness beats micro-optimization.

Real-World Use Cases

State Management Snapshots

// Save a deep copy of app state before a risky operation
const snapshot = structuredClone(appState);

try {
  performRiskyMigration(appState);
} catch (error) {
  // Rollback to snapshot
  Object.assign(appState, snapshot);
}

Immutable Updates in Reducers

function reducer(state, action) {
  const next = structuredClone(state);

  switch (action.type) {
    case 'UPDATE_USER':
      next.user.name = action.payload.name;
      return next;
    default:
      return state;
  }
}

Test Fixtures

const baseFixture = {
  user: { id: 1, name: 'Test User', created: new Date() },
  items: new Map([['a', 1], ['b', 2]])
};

// Each test gets a fresh, independent copy
beforeEach(() => {
  testData = structuredClone(baseFixture);
});

Common Mistakes

1. Cloning Objects With Functions

structuredClone({ handler: () => {} }) throws a DataCloneError. If your object contains callbacks or methods, strip them before cloning or use a different approach.

2. Expecting Prototype Preservation

Cloned objects are plain Object instances. If you clone a class instance, the clone won’t have the class’s methods or pass instanceof checks.

3. Using structuredClone for Shallow Copies

If your object is flat (no nested objects), { ...obj } is 10-100x faster. structuredClone() traverses the entire object graph — unnecessary overhead for a flat structure.

Best Practices

  • Default to structuredClone() for any deep copy. It’s correct by default and handles edge cases JSON can’t.
  • Use spread for shallow copies. Flat objects don’t need deep cloning.
  • Strip functions before cloning if your object mixes data and callbacks. Clone the data, reattach the functions after.
  • Use the transfer option for large ArrayBuffer data to avoid expensive copies.
  • Remove Lodash’s cloneDeep from your bundle if you’re only using it for deep copies of plain data. structuredClone() is a native, zero-dependency replacement.

Wrapping Up

structuredClone() is one of those APIs that should have existed from day one. It deep copies objects correctly, handles all the types that JSON.parse(JSON.stringify()) breaks on, supports circular references, and is available everywhere — browsers, Node.js, Deno, Bun, workers.

The JSON hack had a good run. Time to retire it.

FAQ

Is structuredClone available in Node.js?

Yes, since Node.js 17. It’s a global function — no import needed. It’s also available in Deno, Bun, and all modern browsers.

Can I polyfill structuredClone?

For older environments, the core-js library includes a polyfill. But in 2026, all actively supported platforms have native support. You’re unlikely to need a polyfill unless you’re supporting very old Node.js versions.

Does structuredClone copy Symbol-keyed properties?

No. Symbol-keyed properties are silently ignored during cloning. Only string-keyed, enumerable own properties are copied. This is by design in the structured clone algorithm.

Should I use structuredClone in React state updates?

It depends. For complex nested state with Maps or Sets, yes. For simple objects, prefer the spread pattern ({ ...state, key: newValue }) or Immer, which is optimized for React’s update patterns and doesn’t clone unchanged branches.

editor's pick

latest video

news via inbox

Nulla turp dis cursus. Integer liberos  euismod pretium faucibua

Leave A Comment