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.
What JSON serialization actually does to your data
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:
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 — the built-in that handles all of it
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.
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.
Circular references — handled
The thing that makes JSON.parse(JSON.stringify()) throw, structuredClone handles correctly:
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.
The one thing it won't clone
structuredClone follows the same constraints as postMessage. It clones data, not behavior. Functions are not transferable, so they throw:
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.
Browser support
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.
What to do now
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:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Top comments (0)