Articles

structuredClone() Explained: Deep Copies in JS

structuredClone() is a built-in JavaScript function for deep-copying values, including cycles and typed arrays, without the workarounds JSON tricks require.

Takina Takina · · 4 min read
JavaScript code displayed on a dark editor theme

structuredClone() is a global JavaScript function that creates a deep copy of a value — objects, arrays, Maps, Sets, dates, typed arrays, and more — without the caller needing a library or a serialization hack. It’s built into the runtime (browsers and Node.js alike), and it correctly handles cases that the old JSON.parse(JSON.stringify(x)) trick gets wrong.

Why deep copies are hard in JavaScript

Assigning an object to a new variable copies a reference, not the data:

const original = { user: { name: "Alice" } };
const shallow = original;
shallow.user.name = "Bob";
console.log(original.user.name); // "Bob" — same object underneath

A shallow copy via spread ({ ...original }) or Object.assign only copies the top level — nested objects are still shared references. To fully decouple a copy from its source, every nested object needs its own copy too. That’s a deep clone, and for years JavaScript had no built-in way to do it correctly.

The JSON round-trip problem

The common workaround was JSON.parse(JSON.stringify(value)). It’s concise, but it silently breaks on anything JSON can’t represent:

  • undefined values, functions, and symbols are dropped entirely.
  • Date objects become strings, not dates.
  • Map, Set, RegExp, and typed arrays serialize incorrectly or throw.
  • Circular references throw a TypeError immediately.
  • NaN and Infinity become null.

For simple plain-data objects this is fine. For anything richer, it’s a source of quiet bugs — a cloned Date that’s actually a string, a Map that’s now {}.

What structuredClone() handles correctly

const original = {
  createdAt: new Date(),
  tags: new Set(["a", "b"]),
  scores: new Map([["alice", 10]]),
  buffer: new Uint8Array([1, 2, 3]),
};

const copy = structuredClone(original);

copy.createdAt instanceof Date; // true
copy.tags instanceof Set;       // true
copy.scores.get("alice");       // 10

It implements the structured clone algorithm — the same algorithm browsers already used internally to pass data to Web Workers via postMessage and to store values in IndexedDB. structuredClone() just exposes that algorithm directly as a callable function instead of requiring you to round-trip through a worker or a database.

It also handles circular references, since the algorithm tracks object identity during the walk rather than serializing to a flat string:

const node = { name: "root" };
node.self = node;
const cloned = structuredClone(node); // works fine
cloned.self === cloned; // true

What it can’t clone

Structured clone is not a universal copy mechanism. It throws a DataCloneError on:

  • Functions and class methods (the algorithm has no way to serialize executable code).
  • DOM nodes.
  • Property accessors (getters/setters) — only the current value is copied, and the resulting property is a plain data property.
  • Error objects lose their prototype chain in most engines (this varies by implementation and is worth testing if you rely on instanceof after cloning).

If an object contains any of these, you’ll need a manual clone or a library that lets you customize per-type behavior.

structuredClone() vs the JSON trick vs manual cloning

| | structuredClone() | JSON.parse(JSON.stringify()) | Manual/recursive clone | |---|---|---| | Dates, Maps, Sets | Preserved correctly | Broken (dates → strings, Maps/Sets → {}) | Correct if you write the cases | | Circular references | Supported | Throws | Supported if you write the cases | | Functions | Throws DataCloneError | Silently dropped | Depends on implementation | | undefined values | Preserved | Dropped | Depends on implementation | | Performance | Native, fast | Fast for small plain objects | Slowest, but fully customizable |

For most application state — form data, API responses stored in memory, values passed between a main thread and a Web WorkerstructuredClone() is the right default. Reach for a library only when you need to clone class instances with custom prototypes or skip specific properties during the copy.

A practical use: resetting state without a fetch

A common pattern is cloning a “known good” object to reset editable UI state without re-fetching from the server:

const serverState = await fetchConfig();
let draft = structuredClone(serverState);

function resetDraft() {
  draft = structuredClone(serverState);
}

Because the clone is fully independent, mutating draft — even deeply, editing nested objects — never touches serverState. That’s the property a shallow copy or a naive reference assignment can’t give you, and it’s the same class of problem that comes up with immutable state updates; see optimistic UI updates for a related pattern that depends on cleanly separating “current” state from “pending” state.

Transferring instead of copying

For large binary data — an ArrayBuffer backing a big typed array, for instance — cloning still means copying every byte, which costs time and memory proportional to the size of the data. When the sending side no longer needs the original after handing it off, structuredClone() accepts a second argument that transfers ownership of specific objects instead of copying them:

const buffer = new ArrayBuffer(1024 * 1024 * 16); // 16 MB
const copy = structuredClone(buffer, { transfer: [buffer] });

buffer.byteLength; // 0 — ownership moved, not copied

This is the same transfer mechanism used when passing data to a Web Worker via postMessage, and it matters most when moving large buffers between a main thread and a worker, where an actual byte-for-byte copy would otherwise block the thread proportional to the buffer’s size. It’s a niche case compared to ordinary object cloning, but worth knowing about before reaching for a copy when a transfer would do.

The takeaway

structuredClone() gives JavaScript a native, correct deep-copy function that handles dates, Maps, Sets, typed arrays, and circular references — all cases the JSON.stringify round-trip gets wrong. It can’t clone functions, DOM nodes, or getters/setters, so reach for a manual or library-based clone when an object needs that. For everyday application state, it’s simpler and safer than the workarounds it replaces.

Takina Takina · · 5 min read

Promise.all() vs allSettled() vs race() Compared

Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

JavaScript Spread vs Rest Operators, Explained

The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

ResizeObserver API Explained: Watching Element Size

The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.

#Web Development #JavaScript #Frontend