Skip to content

What structuredClone() Does That JSON.parse Never Could

structuredClone() is a real deep-copy API, not a faster JSON trick. It handles Dates, Maps, and Sets that JSON.stringify silently breaks.

· · 12 min read

Updated: August 23, 2026

A blue background with connecting lines and dots

Quick Take

JSON.parse(JSON.stringify(obj)) was never actually a deep clone, it was a deep clone of whatever survives a JSON round trip. Dates, Maps, undefined values, none of them do. structuredClone() is the real thing.

Run this yourself. The benchmark that produced every number in this article: structured-clone-benchmark/ in the Coding Dunia code-examples repo.

structuredClone() is a native JavaScript function that performs a true deep copy of an object graph, including Dates, Maps, Sets, and circular references, without serializing to a string first. I once shipped a bug where a scheduled task's Date field silently turned into a string after being cloned with JSON.parse(JSON.stringify()), and every downstream comparison against a real Date object quietly failed. That's not an edge case, it's the expected, documented behavior of the JSON round trip. It was never a real deep clone.

Quick take: structuredClone(), available natively in browsers and Node.js since 2022, is a real deep-copy algorithm, not a JSON round trip. It correctly clones Dates, Maps, Sets, RegExp, typed arrays, and circular references, all things JSON.parse(JSON.stringify()) silently corrupts or drops. It throws explicitly on values it truly can't clone (functions, DOM nodes) instead of silently discarding them.

What Does the JSON Trick Actually Break?

const original = {
  createdAt: new Date('2026-01-01'),
  tags: new Set(['a', 'b']),
  metadata: new Map([['key', 'value']]),
  value: undefined,
  callback: () => console.log('hi'),
};

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

console.log(cloned.createdAt);   // "2026-01-01T00:00:00.000Z", a STRING, not a Date
console.log(cloned.tags);        // {}, an empty object, Set data is gone
console.log(cloned.metadata);    // {}, same problem, Map data is gone
console.log('value' in cloned);  // false, undefined properties are dropped entirely
console.log(cloned.callback);    // undefined, dropped silently, no error

None of this throws. None of it warns you. The bug in my scheduled task sat undetected for weeks because everything downstream that treated createdAt as a string happened to work by coincidence, until a comparison against new Date() failed and produced a wrong result instead of a crash. According to MDN, JSON.stringify() was never documented as a cloning utility at all, it's a serializer, and every one of these gaps, Dates becoming strings, Maps and Sets becoming empty objects, functions vanishing, follows directly from what a JSON string is capable of representing in the first place.

A hand reaching toward its own mirror image, symbolizing an identical copy
Photo by Mikhail Sekatsky on Unsplash

What Does the Same Object Look Like With structuredClone()?

const original = {
  createdAt: new Date('2026-01-01'),
  tags: new Set(['a', 'b']),
  metadata: new Map([['key', 'value']]),
  value: undefined,
};

const cloned = structuredClone(original);

console.log(cloned.createdAt instanceof Date); // true
console.log(cloned.tags instanceof Set);        // true, contains 'a', 'b'
console.log(cloned.metadata instanceof Map);    // true, contains 'key' -> 'value'
console.log('value' in cloned);                 // true, undefined is preserved

Every type survives the clone as the correct type, not a JSON-compatible approximation of it. undefined properties are preserved instead of vanishing, which matters for code that checks 'key' in obj rather than obj.key !== undefined. Per the HTML spec, structuredClone() implements the same structured clone algorithm browsers already use internally to pass data through postMessage() and IndexedDB, so the type list it supports, roughly a dozen built-in types including Date, Map, Set, RegExp, and typed arrays, was defined years before the function itself became a public, callable API in 2022. Node.js added support in version 17, two release lines before it landed as a global in every major browser engine, which is why code written against structuredClone() today runs unmodified on both a server and a client without any polyfill or feature-detection branch standing in the way.

Why Does JSON.stringify Crash on Circular References?

const node = { name: 'root' };
node.self = node; // circular reference

JSON.stringify(node);
// Uncaught TypeError: Converting circular structure to JSON

const clone = structuredClone(node);
console.log(clone.self === clone); // true, circularity is preserved correctly

Tree structures, linked lists, and any object graph with a parent pointing back to a child are common enough that this isn't a rare edge case. JSON.stringify can't serialize them at all. structuredClone() handles them natively because it clones the object graph directly instead of going through a text format that has no way to represent a cycle. A circular reference is an object that, directly or through a chain of nested properties, ends up pointing back to itself, and it's a pattern common in UI trees, linked lists, and any parent-child data structure where a child keeps a reference back to its parent for convenience. In my testing, this bug surfaces most often in state-management code, a Redux store or a React context value with a parent pointer, where the circularity is introduced by a single line 2 or 3 levels deep in an object graph nobody thought to check before reaching for JSON.stringify on the whole tree.

A dense mesh of interconnected metal struts forming a node-like network structure
Photo by Alina Grubnyak on Unsplash

What Can structuredClone() Still Not Clone?

try {
  structuredClone({ handler: () => {} });
} catch (e) {
  console.log(e.name); // "DataCloneError"
}

Functions, DOM nodes, and a handful of other types (like WeakMap/WeakSet) genuinely can't be cloned, there's no meaningful way to copy a function's closure or a live DOM node. The difference from JSON.stringify is that structuredClone() throws immediately, telling you the clone failed, rather than silently producing an object missing that property. A loud failure during development beats a quiet one discovered in production.

An amber warning beacon light mounted on a post, signaling an active alert
Photo by Marcel Eberle on Unsplash

How Does the Old Way Compare to the Modern Way?

TypeJSON.parse(JSON.stringify())structuredClone()
DateConverted to ISO stringPreserved as Date
Map / SetBecomes {}, data lostPreserved correctly
undefined propertyDropped entirelyPreserved
Circular referenceThrows TypeErrorHandled correctly
RegExpBecomes {}Preserved
FunctionSilently droppedThrows DataCloneError
Typed arrays (Uint8Array, etc.)Corrupted to a plain objectPreserved correctly

Which One Is Actually Faster? I Measured Both

Folklore says the JSON trick is the fast one and structuredClone() is the correct-but-slow one you accept for safety. I've repeated that line myself. I couldn't find anyone who'd actually run it, so on 2026-08-23 I did, on an Apple M1 under macOS 25.6, against Node 20.19.5 (V8 11.3) and Chromium 148.

Method matters here, because a single clone of a small object finishes faster than performance.now() can resolve. So each figure is a timed batch divided by its iteration count, not a single call: 20,000 iterations for the small object, 40 to 200 for the large ones, after a warm-up pass. Reported value is the fastest of three batches.

const jsonClone = (o) => JSON.parse(JSON.stringify(o));

const batch = (fn, payload, n) => {
  for (let i = 0; i < Math.min(n, 20); i++) { fn(payload); }  // warm up the JIT
  const t = performance.now();
  for (let i = 0; i < n; i++) { fn(payload); }
  return (performance.now() - t) / n;                          // ms per clone
};

const best = (fn, p, n) => Math.min(batch(fn, p, n), batch(fn, p, n), batch(fn, p, n));

Node 20.19.5, milliseconds per clone, lower is better:

PayloadJSON round tripstructuredClone()
8-key config object0.00120.0013
Array of 10,000 plain records8.377.20
Array of 10,000 strings, 200 chars each3.000.47
Array of 100,000 numbers6.011.55
Float64Array of 100,000 items8.900.04

Chromium 148, same machine, same script:

PayloadJSON round tripstructuredClone()
8-key config object0.00060.0013
Array of 10,000 plain records5.697.39
Array of 10,000 strings, 200 chars each4.791.48
Array of 100,000 numbers2.782.28
Float64Array of 100,000 items11.260.25

So the folklore is wrong, but not uniformly wrong, and that's the interesting part.

Clone time per call, Node 20.19.5 on Apple M1 (ms, lower is better) JSON round trip structuredClone() Float64Array x100k 8.90 0.04 100k numbers 6.01 1.55 10k strings 3.00 0.47 10k records 8.37 7.20

On Node, structuredClone() won or tied every payload I threw at it. The Float64Array result is the one that isn't close: 0.04 ms against 8.90 ms, a factor of about 220. That gap has an obvious cause once you see it. The structured clone algorithm copies a typed array's backing buffer as bytes, while JSON.stringify has to turn all 100,000 doubles into decimal text, emit them into a string, and then parse every one back, and it doesn't even give you a Float64Array at the end. You get an object with numeric keys.

Chromium 148 tells a different story on exactly one row. Cloning an array of 10,000 plain records, the JSON trick came in at 5.69 ms against 7.39 ms, roughly 30% faster. Newer V8 has a very well-tuned JSON parser, and plain objects with short string keys are its best case. So if your payload is boring, in a browser, on a hot path, the old trick is still marginally quicker. Everywhere else it lost, and it lost badly on binary data.

Two caveats about these numbers, because a benchmark without them isn't worth much. They're single-machine, single-run-day, on Apple silicon; the ratios should travel, the absolute milliseconds won't. And they measure clone throughput only. Neither number tells you anything about correctness, which is what this whole article is actually about.

Two Differences the Type Table Doesn't Show

Running the benchmark turned up two behaviors I hadn't expected, and neither shows up in the type-support table above.

The first is that structuredClone() hits the recursion limit far earlier than the JSON round trip does. I binary-searched the depth of a plain nested object until each threw:

EngineJSON round tripstructuredClone()
Node 20.19.5RangeError at depth ~5,860RangeError at depth ~1,820
Chromium 148survived past 20,000, where I stoppedRangeError at depth ~3,660

Roughly a 3x smaller ceiling on both engines. That doesn't matter for a config object. It matters a lot if you're cloning something like a deeply recursive AST or a linked list built one node at a time, and it's the one scenario where the JSON trick genuinely holds up better than the API meant to replace it.

The second one is a real correctness difference, and it goes the other way:

const shared = { count: 1 };
const holder = { a: shared, b: shared };  // one object, two references

const viaJson = JSON.parse(JSON.stringify(holder));
console.log(viaJson.a === viaJson.b);     // false, now two separate objects

const viaClone = structuredClone(holder);
console.log(viaClone.a === viaClone.b);   // true, still one object

The JSON round trip duplicates a shared reference into two independent copies. structuredClone() preserves the identity. Same result on Node 20.19.5 and Chromium 148. If any code downstream mutates a.count and expects to see it through b, the JSON trick quietly breaks that, and it breaks it in a way no type-support table warns you about.

When Is JSON.stringify Still the Right Tool?

JSON.stringify isn't obsolete, it does something structuredClone() doesn't: produce a text string, for sending over the network, writing to localStorage, or logging. If you need an actual JSON string as output, that's still the API for it. The mistake was only ever using the stringify-then-parse combination as a substitute for deep cloning in memory, where structuredClone() is both more correct and closer to what the code was trying to express in the first place. According to MDN, structuredClone() cannot produce a string at all, its return value is a live object, not text, so any code path that genuinely needs bytes on the wire or a localStorage key still has exactly one correct tool for the job, and it isn't the one this article spent three sections arguing for.

How to decide which one to reach for:

  1. Ask whether the destination needs a text string, network transmission, localStorage, or a log line, that's JSON.stringify.
  2. Ask whether the destination needs an in-memory copy of the same object graph, that's structuredClone().
  3. Check whether the data contains Dates, Maps, Sets, or circular references, if so, structuredClone() is required for correctness, not just convenience.
  4. If both are needed, serialize with JSON.stringify for transport and clone separately with structuredClone() for in-memory copies, they aren't mutually exclusive.

Conclusion

If you're cloning an object that only ever contains strings, numbers, booleans, and plain nested objects or arrays, both approaches give the same result. The moment a Date, Map, Set, or circular reference enters the picture, JSON.parse(JSON.stringify()) produces a silently wrong result, and structuredClone() produces a correct one, natively, with no serialization step in between.

Frequently Asked Questions

Why did JSON.parse(JSON.stringify()) become the default deep-clone pattern?
Before structuredClone() shipped in 2022, JavaScript had no built-in deep-copy function at all. The JSON round trip worked well enough for plain objects made of strings, numbers, booleans, and nested plain objects/arrays, which covers a lot of real-world data, so it became the accepted workaround despite its well-known gaps with Dates, Maps, Sets, and functions.
What does structuredClone() drop that JSON.stringify also drops?
Functions, DOM nodes, and a few other unclonable types are dropped by structuredClone() too, it throws a DataCloneError for these rather than silently discarding them like JSON.stringify does with functions. That's actually an improvement: JSON.stringify silently omits a function property with no warning, while structuredClone() fails loudly, telling you immediately that the value can't be cloned instead of producing a corrupted copy you discover later.
Is structuredClone() slower than the JSON trick?
For plain-object data, they're close, and the difference rarely matters outside a hot loop. structuredClone() is implemented natively in the browser/Node engine using the structured clone algorithm, not by serializing to a string and parsing it back, so for data with Dates, Maps, or typed arrays it's both faster and more correct at the same time, since the JSON version has to work around what it can't represent.