⚡ The ‘Safer’ Clone That Started Throwing Where JSON Never Complained
Swapping JSON.parse(JSON.stringify(obj)) for the newer, purpose-built structuredClone() is a very reasonable upgrade – it correctly clones Dates, Maps, Sets, and typed arrays that the JSON round-trip quietly mangles. But structuredClone is also genuinely STRICTER: where JSON.stringify silently drops functions, undefined values, and symbols (or throws only on a circular reference), structuredClone throws a DataCloneError the instant it encounters a function, a DOM node, or anything else it fundamentally cannot serialize – turning data that used to survive the old approach (minus the parts it silently dropped) into a hard runtime error instead.
🔎 The Problem
const config = {
retries: 3,
onError: (err) => console.error(err), // A function property
createdAt: new Date(),
};
// Old approach: "worked" - silently dropped onError entirely, and
// turned createdAt into a plain ISO string instead of a real Date.
const oldClone = JSON.parse(JSON.stringify(config));
console.log(oldClone.onError); // undefined - just quietly gone
console.log(oldClone.createdAt); // a string, not a Date anymore
// New approach: throws immediately instead of silently dropping onError.
const newClone = structuredClone(config);
// Uncaught DOMException: Failed to execute '"'"'structuredClone'"'"' -
// (err) => ... could not be cloned.
✅ Fix: Separate What Actually Needs Cloning From What Doesn’t
- Functions, class instances with methods, and DOM references were never genuinely being ‘cloned’ by the JSON approach – they were being silently discarded, so the honest fix is to exclude them from the object being cloned in the first place (clone only the plain data, and reattach any function/behavior separately afterward), rather than looking for a clone function that tolerates them.
- structuredClone’s error is specific about what it couldn’t clone, which makes it a genuinely useful signal during a migration away from the JSON round-trip – each DataCloneError it throws is pointing at a spot where the old code was silently losing data that nobody previously noticed was missing.
- For an object that intentionally mixes data with behavior (a class instance with both properties and methods), a dedicated clone() method on the class itself – copying only the fields that matter and reconstructing the rest – is more correct than reaching for either generic clone approach, since neither one actually understands what ‘a correct copy of this specific type’ means.
⚠️ Why the Old Code ‘Worked’ at All
- JSON.stringify’s behavior toward things it can’t represent (silently omitting the property entirely, for a function or undefined; throwing only for a genuine circular reference) was never actually correct – it just failed in a way that didn’t throw, which is a very different thing from actually succeeding.
- A codebase that’s relied on the JSON round-trick for cloning for a long time can have quite a few objects where ‘the clone lost a property’ was already happening silently and nobody noticed, specifically because nothing downstream ever needed that particular property from the cloned copy – switching to structuredClone is often the first time this becomes visible at all.
JSON.stringify never actually cloned everything – it just failed quietly on the parts it couldn’t handle, and structuredClone is simply the first version of this idea honest enough to say so out loud.
