← blog
JavaScriptJuly 10, 2026 · 4 min read

You've been deep-cloning objects with a JSON hack. structuredClone does it right.

JSON.parse(JSON.stringify(obj)) silently corrupts Dates, Maps, Sets, and undefined values. structuredClone is the native replacement — no library, no import, no surprises.

Parsa Jiravand · Frontend engineer · building bestpractic
You've been deep-cloning objects with a JSON hack. `structuredClone` does it right.

The JSON.parse(JSON.stringify(obj)) incantation is in almost every codebase. It works often enough that most developers have reached for it without a second thought. No imports, no library, a deep clone in one line — memorable and good enough for a decade.

"Good enough" is doing a lot of work in that sentence.

JSON.stringify serializes a value to a text string. JSON has a limited type vocabulary: strings, numbers, booleans, null, arrays, and plain objects. Anything outside that set either gets transformed silently or dropped entirely.

Here's what happens when you round-trip a realistic object through JSON:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
const original = { createdAt: new Date('2024-01-15'), config: new Map([['theme', 'dark']]), count: undefined, tags: new Set(['css', 'js']), }; const cloned = JSON.parse(JSON.stringify(original)); console.log(typeof cloned.createdAt); // "string" — not a Date console.log(cloned.config); // {} — not a Map console.log('count' in cloned); // false — undefined is dropped console.log(cloned.tags); // {} — not a Set

The clone exists, but it's not the same shape as the original. A Date became a string — so cloned.createdAt.getMonth() throws a TypeError. A Map became an empty object, stripping away .get(), .has(), and iteration. Undefined properties disappear entirely, which can quietly break code that checks for key presence. Set instances collapse to {}.

Circular references throw outright: JSON.stringify raises TypeError: cyclic object value rather than attempting to handle them.

structuredClone is a global function that deep-clones a value using the Structured Clone Algorithm — the same algorithm the browser uses internally to pass data across web workers, postMessage, and IndexedDB. It's been available since it was designed for exactly this purpose.

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const original = { createdAt: new Date('2024-01-15'), config: new Map([['theme', 'dark']]), count: undefined, tags: new Set(['css', 'js']), }; const cloned = structuredClone(original); console.log(cloned.createdAt instanceof Date); // true console.log(cloned.createdAt.getMonth()); // 0 (January) console.log(cloned.config instanceof Map); // true console.log(cloned.config.get('theme')); // 'dark' console.log('count' in cloned); // true — undefined is preserved console.log(cloned.tags instanceof Set); // true console.log(cloned.tags.has('css')); // true

No import. No library. The types survive the clone.

Runs right in your browser — poke at it and watch the concept react live.

The thing that makes JSON.parse(JSON.stringify()) throw, structuredClone handles correctly:

JavaScript
1
2
3
4
5
6
const a = { name: 'a' }; a.self = a; // circular reference const cloned = structuredClone(a); console.log(cloned.self === cloned); // true — the graph structure is preserved console.log(cloned === a); // false — it's a distinct object

The algorithm preserves the object graph: shared references inside the original are shared in the clone too. Circular structures don't cause infinite loops or errors — they produce a correctly-structured clone.

structuredClone follows the same constraints as postMessage. It clones data, not behavior. Functions are not transferable, so they throw:

JavaScript
structuredClone({ fn: () => {} }); // TypeError: fn could not be cloned

The same applies to DOM nodes and class instances where the prototype chain matters — the data transfers, but the prototype doesn't, so the clone is a plain object. Error objects, however, are supported and clone correctly.

This isn't a bug. The algorithm is intentionally scoped to values, not objects with identity. For the vast majority of real use cases — API response data, form state, configuration objects, anything you'd pass through postMessage — it's exactly the right scope. If you need to clone class instances and preserve their prototype, a library like lodash.cloneDeep covers that case.

structuredClone landed in Chrome 98, Firefox 94, and Safari 15.4. Baseline 2022 — every browser released in the last three years. Node.js added it in version 17.0.

No typeof structuredClone !== 'undefined' guard needed unless you're targeting something very old. If you're on a current browser or a current Node, it's there.

Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.

Search your codebase for JSON.parse(JSON.stringify. For every match, ask: does this object ever contain a Date, Map, Set, undefined value, or circular reference? If yes, the clone is silently dropping or corrupting data and you should replace it with structuredClone.

If the object is genuinely just nested strings, numbers, and arrays — the JSON round-trip technically works — structuredClone is still clearer about intent, faster in modern engines, and safe when the object shape grows to include a Date field six months from now.

The JSON hack was the available tool for a decade. The built-in is better in every dimension that matters for data cloning, and it's been in every major runtime for three years. Time to use the right primitive.


Thanks for reading! Let's stay connected:

Originally published on dev.to