Stop reaching for JSON.parse(JSON.stringify()) — structuredClone is right there
JSON.parse(JSON.stringify(x)) silently drops Dates, Maps and undefined. structuredClone keeps them, ships everywhere, and costs nothing extra.
The bug arrived as a screenshot: an order form where the delivery date showed up as a string of digits, but only after the user edited a line item. Everything worked on first render. Everything broke on the second.
The cause was one line of copying code, written years earlier and copied into the project from a blog post that had itself copied it from somewhere else:
Read that table as a list of bugs waiting for the right input. The date column is the one that bites most often, because
const draft = JSON.parse(JSON.stringify(order));
That expression is the most widely deployed deep-copy implementation in JavaScript, and it is lossy in ways that stay invisible until they aren't. There has been a real alternative in every major browser and in Node for several years now, and most codebases still haven't reached for it.
What the round trip throws away
Serialising to JSON and parsing it back is not a copy operation. It is a conversion to a data format that supports six types, followed by a conversion back. Anything the format cannot express is either transformed into something else or dropped without warning.| Value | After JSON round trip | After structuredClone |
|---|---|---|
| Date | ISO string | Date |
| Map / Set | Empty object | Map / Set |
| undefined property | Key removed | Key preserved |
| NaN / Infinity | null | NaN / Infinity |
| BigInt | Throws | BigInt |
| RegExp | Empty object | RegExp |
| Circular reference | Throws | Preserved |
| Typed array | Plain object of indices | Typed array |
new Date(isoString) is such a common repair that people stop noticing they are performing it. The undefined row is the sneakiest: a key that vanishes changes the shape of an object, which changes how in checks, destructuring defaults, and spread merges behave downstream.
What structuredClone does instead
The structured clone algorithm has been part of the platform for a long time — it is the mechanism behindpostMessage, IndexedDB storage, and worker communication. What changed is that it became directly callable:
const draft = structuredClone(order);
Because it was designed to move live values between JavaScript realms rather than to produce text, it understands the object graph. Dates stay dates. Maps stay maps. A node that appears twice in the source appears once in the copy, referenced twice, exactly as it was. Cycles are handled rather than thrown at.
It is also available essentially everywhere you are likely to run code: current browsers, Node, Deno, and Bun all expose it as a global. There is no import, no dependency, and no bundle cost.
Where it still refuses
Structured cloning is not magic, and the places it stops are worth memorising, because it throws rather than silently degrading — which is the behaviour you want.- Functions and class methods. Anything callable is not clonable. This is not a limitation to work around; a function closing over a scope cannot meaningfully exist in a copy.
- DOM nodes. Use
node.cloneNode(true)for those. - Prototypes. An instance of your
Orderclass comes back as a plain object carrying the same own properties. The data survives; the identity does not. - Property descriptors. Getters are evaluated and stored as plain values, and non-enumerable properties are skipped.
A note on speed, since someone always asks
The instinct is that a native call must be faster than serialising to a string and parsing it back. In our own measurements on a mid-range laptop, that holds for objects containing dates, maps, or large typed arrays — sometimes by a wide margin, because the JSON path is doing conversion work on every value. For flat objects of short strings and numbers, the two are close enough that the difference disappears into measurement noise. Which means performance is the wrong axis for this decision. Both are fast; only one is correct. Choose on correctness and stop thinking about it.The rule we teach
If you are copying data you own, in memory, in the same process, usestructuredClone. If you are producing text to send over a network or write to disk, use JSON.stringify — that is what it is for, and its constraints are the point. The mistake is not using JSON. The mistake is using a serialiser as a copier and hoping the types you happen to have today are the types you will have next quarter.
The order form, for the record, needed a two-character change. The code review that approved the original line was not careless; the expression looks harmless, and it had worked for three years. That is what makes lossy defaults expensive: they buy you time, at interest.