skip to content

What does the global structuredClone() function do, and how does the copy it produces differ from JSON.parse(JSON.stringify(value))?

level: juniorimportance: must knowfreq 55%

answer

  1. deep copy, not a text round-trip
  2. rich types survive the trip
  3. cycles kept, functions rejected
  4. same rule as postMessage and IndexedDB
  5. Map, Set, Date, ArrayBuffer, Blob

basics

~20 s

structuredClone() returns a deep copy made by the browser's structured clone algorithm. Unlike a JSON round-trip it preserves Map, Set, Date, RegExp, typed arrays, Blobs and cyclic references, but it throws a DataCloneError on functions and DOM nodes.

solid answer

~50 s

`structuredClone(value)` is a global that deep-copies a value using the structured clone algorithm — the same algorithm the browser already uses for `postMessage` and for values written to IndexedDB. It walks the object graph and rebuilds it, so nested objects are genuinely copied rather than shared. Compared with `JSON.parse(JSON.stringify(value))` it is much wider: `Date` stays a `Date` instead of becoming a string, `Map` and `Set` survive as `Map` and `Set` instead of collapsing to `{}`, `ArrayBuffer`, typed arrays, `Blob` and `File` are supported, and cycles are preserved instead of throwing. It is also stricter in one direction: anything the algorithm cannot serialize — a function, a `Symbol`, a DOM node, a `Promise` — throws a `DataCloneError` rather than being silently dropped the way `JSON.stringify` drops it. It has been available as a global in Chrome 98+, Firefox 94+ and Safari 15.4+ since 2022.

code

javascript · 14 lines
javascript
const src = {
  when: new Date(0),
  tags: new Set(['a', 'b']),
  lookup: new Map([['k', 1]]),
  big: 10n,
};

const clone = structuredClone(src);
console.log(clone.when instanceof Date, clone.tags.has('a'), clone.lookup.get('k'), clone.big);
// true true 1 10n

const viaJson = JSON.parse(JSON.stringify({ when: src.when, tags: src.tags, lookup: src.lookup }));
console.log(typeof viaJson.when, viaJson.tags, viaJson.lookup);
// "string" {} {}

go deeper

for a junior

Be able to say that structuredClone() gives a genuine deep copy and that a JSON round-trip loses Date, Map and Set and throws on cycles. Name at least one value kind that makes structuredClone throw.

for a middle

Explain the cloneable set as a specification-defined list rather than a heuristic, and contrast loud refusal (DataCloneError) with JSON's silent data loss. Mention that prototypes and property descriptors do not survive.

for a senior

Show that you recognise the same algorithm behind postMessage, IndexedDB writes and history state, so a value that fails one boundary will fail the others. Note that the clone is synchronous and its cost scales with graph size.

for a principal

Own the message-and-storage contract: decide whether values crossing boundaries are plain transport DTOs or rich domain objects, and make that choice explicit so teams do not discover the clone rules one DataCloneError at a time.

## What the function is `structuredClone(value)` is a global function exposed on `Window` and on worker global scopes. It takes any value and returns a deep copy of it, produced by the **structured clone algorithm** — a serialize-then-deserialize pair defined by the HTML specification. The function is new (Chrome 98, Firefox 94, Safari 15.4, all shipped by early 2022), but the algorithm behind it is old: it is the rule the browser has always used to decide what may cross `postMessage`, what may be written into IndexedDB, and what may be handed to `history.pushState`. Learning it once explains all of those boundaries at the same time. ```js const copy = structuredClone(original); copy.nested.value = 'changed'; // original.nested.value is untouched ``` ## What "deep" actually means here The algorithm walks the whole reachable graph of the input. Every plain object and array is rebuilt, so mutating anything in the copy — at any depth — cannot be observed through the original. This is the property a shallow copy such as `{ ...obj }` or `Object.assign({}, obj)` does not give you: those copy the top-level properties, so a nested object is still the *same* object in both. ## What survives The cloneable set is broad and is defined by the specification, not by the function: - primitives, including `BigInt`, and boxed `String`/`Number`/`Boolean` objects - plain objects and arrays (including sparse arrays) - `Date`, `RegExp` (source and flags; `lastIndex` is not carried over) - `Map` and `Set`, with their keys and values cloned recursively - `ArrayBuffer`, all typed arrays and `DataView` - `Blob`, `File`, `FileList`, `ImageData`, `ImageBitmap` - `Error` and its standard subclasses (`name`, `message`, and `cause`) - cyclic and shared references, which are reproduced as cycles and shared references ## What a JSON round-trip does instead `JSON.parse(JSON.stringify(value))` is a text round-trip, and JSON's data model is far smaller than JavaScript's. A `Date` serializes through its `toJSON` method and comes back as a string. A `Map` or `Set` has no enumerable own properties, so it serializes as `{}` and the contents are simply gone. `undefined`, functions and symbol values are dropped from objects and turned into `null` inside arrays. `NaN` and `Infinity` become `null`. A cycle makes `JSON.stringify` throw. And a `BigInt` makes it throw as well. ```js const src = { when: new Date(0), tags: new Set(['a']) }; structuredClone(src).tags instanceof Set; // true JSON.parse(JSON.stringify(src)).tags; // {} ``` The practical difference is not that one is "better": it is that JSON's losses are *silent* while the clone algorithm's refusals are *loud*. ## Where structuredClone is stricter Anything outside the cloneable set throws a `DOMException` whose `name` is `"DataCloneError"`. Functions, symbols as values, DOM nodes, `Promise`, `WeakMap`/`WeakSet` and proxies all fail this way. So if your object carries a callback, `structuredClone` will not quietly hand you a copy missing that callback — it will refuse the whole operation. Two more things change quietly rather than throwing. Prototypes are not preserved: a class instance comes back as a plain object whose prototype is `Object.prototype`, so its methods are gone. And property descriptors are flattened: only own **enumerable string-keyed** properties are copied, a getter is invoked once and its result stored as an ordinary data property, and symbol-keyed properties are skipped. ## Choosing between them Use `structuredClone` when you want an in-memory deep copy and you control the shape of the value — it is faster than serializing to text and back, and it keeps the rich types. Use `JSON.stringify` when the destination genuinely needs *text*: an HTTP body, a log line, or `localStorage`, whose API only accepts strings. Reach for a library deep-clone only when you need behaviour the algorithm deliberately does not give you, such as preserving class prototypes or copying functions by reference. One last practical note: `structuredClone` is synchronous and runs on the calling thread, so on a very large graph it is not free — the cost is proportional to the size of the graph it has to walk.

  • Other than calling structuredClone() directly, where else does the browser run this same algorithm?
    Anywhere a value crosses a boundary the browser controls: `postMessage` on a window, worker, `MessagePort` or `BroadcastChannel`; values written to IndexedDB; and the state object given to `history.pushState`/`replaceState`. `localStorage` is the odd one out — it stores strings only, which is why JSON is used there.
  • If structuredClone is a deep copy, why does Object.assign or spread not do the same job?
    Both are shallow. `{ ...obj }` copies the top-level own enumerable properties, so any nested object is the *same* object in both the source and the copy — mutating `copy.nested.x` is visible through `original.nested.x`. They also copy symbol-keyed properties and invoke getters, which the clone algorithm treats differently.
  • Does structuredClone preserve the prototype of a class instance?
    No. The clone is an ordinary object with `Object.prototype` as its prototype, so `instanceof` fails and prototype methods are missing. Only own enumerable string-keyed data is carried over. If the receiver needs behaviour, it has to rehydrate — pass plain data and reconstruct the instance on the other side.

saying these in an interview costs you the question

  • Claims structuredClone is just JSON.parse(JSON.stringify()) under the hood
  • Says it copies functions and methods along with data
  • Thinks it is a shallow copy like object spread
  • Believes class instances arrive with their prototype intact
  • Assumes cycles throw the way JSON.stringify throws

context