skip to content

What does structuredClone(value) give you that a JSON.parse(JSON.stringify(value)) round trip does not, and which values does it refuse to clone?

level: middleimportance: must knowfreq 60%

answer

  1. one is a real copy, one is a serialisation
  2. cycles and shared references survive
  3. behaviour cannot be copied
  4. the prototype does not come along
  5. it throws instead of dropping

basics

~20 s

structuredClone performs a real deep copy: it preserves cycles and shared references and reproduces Date, RegExp, Map, Set, ArrayBuffer and typed arrays as themselves. It throws a DataCloneError for functions and symbols, and it does not preserve prototypes.

solid answer

~50 s

A JSON round trip is a serialisation, not a copy: it only knows the JSON data model, so `Date` becomes a string, `Map` and `Set` become `{}`, `undefined` and function-valued properties vanish, and a cycle throws. `structuredClone` implements the structured clone algorithm, which walks the object graph directly. It reproduces `Date`, `RegExp`, `Map`, `Set`, `ArrayBuffer`, typed arrays and `Error` objects as the same kinds of things, it preserves cycles, and it preserves shared references — if two properties pointed at one object, the clone's two properties point at one clone. What it will not do is carry behaviour: a function or symbol anywhere in the graph makes the whole call throw a `DataCloneError`, and a class instance comes back as a plain object with the same data but no prototype, so its methods and `instanceof` are gone.

code

javascript · 24 lines
javascript
const shared = { id: 1 };
const state = {
  when: new Date('2020-01-01'),
  tags: new Set(['a', 'b']),
  by: new Map([['k', 1]]),
  left: shared,
  right: shared,
};
state.self = state;

const copy = structuredClone(state);

console.log(copy.when instanceof Date); // true
console.log(copy.tags.has('a'));        // true
console.log(copy.by.get('k'));          // 1
console.log(copy.self === copy);        // true
console.log(copy.left === copy.right);  // true
console.log(copy.left === shared);      // false

try {
  structuredClone({ run() {} });
} catch (e) {
  console.log(e.name); // "DataCloneError"
}

go deeper

for a junior

Know that structuredClone is the built-in deep copy and that the JSON round trip is not one: dates, Maps and Sets do not survive it, and cycles make it throw.

for a middle

Be ready to list what the clone algorithm reproduces, explain that cycles and shared references are preserved, and state precisely what happens to a function in the graph and to a class instance's prototype.

for a senior

Demonstrate judgment about where copies belong at all — which boundaries need a defensive snapshot, and how to shape state so that data is cloneable and behaviour lives elsewhere.

for a principal

Own the data-shape rule for the codebase: state that crosses a clone, a worker or a storage boundary must be plain serialisable data, with rehydration into rich objects as an explicit, tested step rather than an assumption.

## The two things people mean by "copy" A **shallow** copy (`{ ...obj }`, `Object.assign({}, obj)`) makes a new outer object whose properties still point at the same nested objects. A **deep** copy makes new objects all the way down, so mutating the copy cannot be observed through the original. The JavaScript standard library had no deep-copy function for a long time, which is why `JSON.parse(JSON.stringify(x))` became folklore. ## Why the JSON round trip is not a deep copy It is a *serialisation followed by a parse*, and it can only express what the JSON data model can express. Going through it, at the level that matters for copying: - values with no JSON representation — `undefined`, functions, symbols — are dropped from objects and become `null` inside arrays; - a `Date` comes back as a string, because serialising it produced a string; - `Map`, `Set`, `RegExp` and class instances come back as ordinary objects, usually empty ones, because their state is not in enumerable own properties; - `BigInt` throws; - a cycle throws, because there is no way to write one down. So the round trip works only for graphs that were already plain JSON-shaped data, and fails *silently* for most of the interesting cases — which is what makes it dangerous rather than merely limited. ## What structuredClone does instead `structuredClone(value)` runs the **structured clone algorithm**, which walks the live object graph and builds a parallel graph of the same kinds of objects. Types it reproduces as themselves include `Date`, `RegExp`, `Map`, `Set`, `ArrayBuffer`, `DataView`, the typed arrays, and `Error` objects; in a browser it also handles host types marked serializable, such as `Blob`, `File` and `ImageData`. It handles graph shape, not just tree shape: ```js const state = { when: new Date(), tags: new Set(['a']), by: new Map([['k', 1]]) }; state.self = state; // a cycle const shared = { id: 1 }; state.left = shared; state.right = shared; // two paths to one object const copy = structuredClone(state); copy.when instanceof Date; // true copy.tags.has('a'); // true copy.self === copy; // true — cycle preserved, not infinite copy.left === copy.right; // true — sharing preserved copy.left === shared; // false — but it is a new object ``` That cycle-and-sharing preservation is the property a hand-written recursive clone usually gets wrong, and it is the strongest single reason to prefer the built-in. ## What it refuses: DataCloneError The algorithm has no way to copy behaviour, so it does not try. A function or a symbol anywhere in the graph makes the call throw a `DataCloneError` (a `DOMException`), and the throw is **all-or-nothing**: you do not get a partial clone with the offending property omitted. ```js structuredClone({ n: 1, onDone() {} }); // DataCloneError: onDone() {} could not be cloned. ``` Browser DOM nodes are likewise not cloneable and throw. Note that this differs from `JSON.stringify`, which drops functions quietly — structuredClone's loudness is a feature: it tells you the value you were about to copy contained something a copy cannot represent. ## What it changes silently The important silent transformation is **prototypes are not preserved**. Cloning an instance of your own class produces a plain object carrying the same own data properties, with `Object.prototype` as its prototype: ```js class User { constructor(name) { this.name = name; } greet() { return `hi ${this.name}`; } } const clone = structuredClone(new User('ada')); clone.name; // 'ada' clone instanceof User; // false clone.greet; // undefined ``` In the same vein, accessor properties are read and stored as plain data, and property attributes such as non-enumerable or non-writable are not carried across. Private class fields (`#x`) are not own properties at all, so they do not survive either. `structuredClone` also ignores `toJSON()` — that hook belongs to JSON serialisation and has no role in the clone algorithm. ## Where it comes from and where it is available `structuredClone` is a global function defined by the HTML specification rather than by ECMAScript, which is why you will not find it in a language reference under `Object`. The underlying algorithm has been in the platform for years as the mechanism behind `postMessage` and IndexedDB storage; exposing it as a callable function is the recent part. It is available in evergreen browsers from early 2022 and in Node from version 17. ## How to decide Use `structuredClone` when you need a deep copy of *data* — state snapshots for undo, defensive copies before a mutating routine, detaching a value from a reactive store. Use a manual or library clone when the graph contains behaviour or when you must preserve class identity. Use `JSON.parse(JSON.stringify(x))` essentially never as a clone: if the data really is JSON-shaped, structuredClone copies it correctly too and faster; if it is not, the JSON round trip corrupts it quietly.

  • Two properties of an object point at the same nested object. What is true of them after structuredClone?
    They still point at one object — a single new clone reachable from both paths. The algorithm keeps a memory of what it has already cloned, which is the same mechanism that lets it terminate on cycles. A JSON round trip loses this: each path is serialised independently, so the parse produces two separate objects with equal contents.
  • Does structuredClone use toJSON() the way JSON.stringify does?
    No. `toJSON()` is a hook of the JSON serialisation algorithm only. The structured clone algorithm never calls it, so a class that relies on `toJSON()` to flatten itself for the wire gets no such treatment here — its own data properties are copied and its prototype is dropped.
  • Why is structuredClone throwing on a function arguably better than JSON.stringify silently dropping it?
    Because a dropped function turns into a bug far from its cause: something later calls `copy.onDone()` and gets a TypeError with no hint that a copy removed it. Throwing at the copy site names the offending property immediately, and it forces an explicit decision — restructure the value so data and behaviour are separate, or clone it some other way.

saying these in an interview costs you the question

  • Says JSON.parse(JSON.stringify(x)) is a proper deep copy
  • Expects class methods or instanceof to survive a clone
  • Thinks a cycle causes structuredClone to hang or overflow
  • Assumes non-cloneable properties are skipped rather than thrown on
  • Believes structuredClone respects toJSON()

context