JavaScript structuredClone() — The Deep Copy You've Been Waiting For

tutorialApril 13, 2026· 5 min read

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"); // false

One function. No options. No gotchas. It just works.

Supported Types

structuredClone() handles the full structured clone algorithm:

TypeSupportedNotes
Plain objects & arraysDeep cloned recursively
DatePreserved as Date instances
RegExpFlags preserved
Map & SetKeys and values deep cloned
ArrayBufferNew buffer with copied bytes
TypedArray (Uint8Array, etc.)New buffer, same type
DataViewBacked by new ArrayBuffer
BlobNew Blob, same data
FileName and metadata preserved
ErrorMessage and type preserved
undefinedNot stripped (unlike JSON)
Infinity, NaNPreserved
ImageDataPixel data deep cloned
FunctionsThrows DataCloneError
DOM nodesThrows DataCloneError
SymbolsThrows DataCloneError
WeakMap / WeakSetThrows 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 works

Snapshot 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); // ❌ DataCloneError

If 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 correctly

This 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 preserved

Plain 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

ApproachUse When
structuredClone()Deep copying plain data objects with Maps, Sets, Dates, ArrayBuffers
Spread ({...obj})Shallow clone is sufficient
JSON.parse/JSONifyYou only have JSON-safe data and need widest compat
Lodash cloneDeepObjects contain functions, class instances, or custom descriptors
Custom clonerYou 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, and undefined
  • 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