JavaScript structuredClone and Deep Copying
Understanding structuredClone and deep copying in JavaScript
Making a copy of an object sounds simple until you realise that a shallow copy shares references to nested objects. Mutate the original after copying, and your copy changes too. This is one of the most common sources of unexpected behaviour in JavaScript. JavaScript structuredClone is the built-in solution: it performs a deep copy of any structured-cloneable value in a single function call.
What is deep copying?
A shallow copy copies only the top-level properties of an object. Nested objects, arrays, and other reference types still point to the original.
const original = { items: [1, 2, 3] };
const shallow = Object.assign({}, original);
shallow.items.push(4);
console.log(original.items); // [1, 2, 3, 4] — the original was mutated!
A deep copy duplicates every nested value recursively, so changes to the clone never affect the original.
Introducing structuredClone
structuredClone is a global function added to JavaScript via the WHATWG structured clone algorithm. It landed in browsers around March 2022 and is also available in Node.js 17+, Deno, and Bun.
const clone = structuredClone(value);
const clone = structuredClone(value, { transfer: [] });
Parameters:
value— any structured-cloneable JavaScript value.options.transfer(optional) — an array of transferable objects (ArrayBuffer,MessagePort,ImageBitmap, etc.) that are moved from the source to the destination. The original becomes detached.
Returns: a deep clone of the value. Throws DataCloneError if a value cannot be cloned.
Basic usage
const original = {
name: 'Alice',
scores: [98, 85, 91],
metadata: { role: 'admin', active: true }
};
const clone = structuredClone(original);
clone.scores.push(77);
clone.metadata.role = 'guest';
console.log(original.scores); // [98, 85, 91] — unchanged
console.log(original.metadata.role); // 'admin' — unchanged
console.log(clone.scores); // [98, 85, 91, 77]
console.log(clone.metadata.role); // 'guest'
The clone is completely independent from the original. No shared references.
Types structuredClone handles well
The structured clone algorithm preserves the type of most common JavaScript values:
| Type | Notes |
|---|---|
| Primitives | string, number, bigint, boolean, null, undefined |
Object, Array | Deep cloned |
Date | Cloned as Date |
RegExp | Cloned as RegExp |
Map / Set | Keys/values or elements deep-cloned |
TypedArray, ArrayBuffer | Cloned |
Error | Cloned as Error (name, message, cause preserved) |
Blob, File, FileList | Cloned |
BigInt | Fully supported (unlike JSON methods) |
Example with complex types:
const complex = {
created: new Date('2024-01-01'),
data: new Map([['key', 'value']]),
ratio: Infinity,
id: BigInt(9007199254740991),
pattern: /test/gi,
active: undefined,
symbol: Symbol('private')
};
const clone = structuredClone(complex);
console.log(clone.created instanceof Date); // true
console.log(clone.data instanceof Map); // true
console.log(clone.id); // 9007199254740991n
console.log(clone.pattern); // /test/gi
console.log(clone.active); // undefined
console.log(clone.symbol); // Symbol(private)
Note that Symbol values as property values are cloned correctly; only Symbol keys are dropped (see the Symbol property keys section below).
Transferring instead of copying
The transfer option moves ownership of large binary buffers rather than copying them. This is useful when passing data between threads or actors and you no longer need the original.
const originalBuffer = new ArrayBuffer(16);
const view = new Uint8Array(originalBuffer);
view[0] = 42;
const clonedBuffer = structuredClone(originalBuffer, {
transfer: [originalBuffer]
});
console.log(originalBuffer.byteLength); // 0 — detached!
console.log(clonedBuffer.byteLength); // 16
console.log(new Uint8Array(clonedBuffer)[0]); // 42
After the transfer, originalBuffer.byteLength is 0. Only ArrayBuffer can be transferred; SharedArrayBuffer cannot.
Common pitfalls
Even though structuredClone handles most types, there are important limits you need to be aware of.
Circular references throw
Unlike some library functions, structuredClone does not handle circular references. It throws DataCloneError.
const obj = { name: 'test' };
obj.self = obj;
structuredClone(obj);
// DataCloneError: cyc
If your data contains circular references, you need a custom implementation or a library.
Even when your data has no cycles, another limitation appears with custom class instances. The structured clone algorithm walks the object graph copying own enumerable properties, but it does not preserve the internal [[Prototype]] link that ties an instance to its constructor. Any code that depends on instanceof checks or prototype methods will behave differently on the clone.
Class instances become plain objects
class Person {
constructor(name) { this.name = name; }
}
const alice = new Person('Alice');
const clone = structuredClone(alice);
console.log(clone instanceof Person); // false
console.log(Object.getPrototypeOf(clone)); // Object.prototype
The properties are copied, but the prototype chain is lost. The clone is a plain object.
Prototype loss is only part of the picture. Since the clone is a plain object, any methods defined on the original’s prototype are gone. But the problem runs deeper — even methods attached directly to the object as own properties cannot be cloned, because the structured clone spec excludes functions entirely.
Methods are not clonable
Functions cannot be cloned; they throw DataCloneError. This means any method attached to an object is lost after cloning.
const obj = {
name: 'Test',
greet() { return `Hello, ${this.name}`; }
};
const clone = structuredClone(obj);
console.log(clone.greet); // undefined
The function restriction means behaviour never survives a round trip. But even for plain data properties, the metadata around those properties — whether they are writable, enumerable, or backed by a getter — is not preserved. The clone always uses default descriptors, which can break code that inspects or relies on property attributes.
Property descriptors are lost
Getters, setters, and property metadata (writable, enumerable, configurable) are not preserved. All cloned properties become simple data properties.
const obj = {};
Object.defineProperty(obj, 'secret', {
get() { return 'hidden'; },
enumerable: false
});
const clone = structuredClone(obj);
console.log(Object.getOwnPropertyDescriptor(clone, 'secret'));
// { value: 'hidden', writable: true, enumerable: true, configurable: true }
Descriptor metadata is one kind of invisible data. Symbol-keyed properties are another. The structured clone algorithm handles Symbol values stored as regular property values without issue. However, Symbol keys sit outside the algorithm’s scope: they are not enumerated during the clone walk, so any property keyed by a Symbol is silently omitted from the result.
Symbol property keys are dropped
Symbols used as object property keys are currently not preserved by the structured clone algorithm.
const key = Symbol('id');
const obj = { [key]: 42, name: 'test' };
const clone = structuredClone(obj);
console.log(Object.getOwnPropertySymbols(clone)); // [] — Symbol key is gone
structuredClone vs JSON methods
Before structuredClone, the common workaround was JSON.parse(JSON.stringify(value)). It works for simple data, but it has significant limitations:
| Limitation | JSON.parse(JSON.stringify()) | structuredClone |
|---|---|---|
| Functions | Silently dropped | Throws DataCloneError |
undefined | Silently dropped | Preserved |
BigInt | Throws TypeError | Fully supported |
Date | Becomes a string | Preserved as Date |
Map / Set | Becomes {} | Preserved with correct type |
RegExp | Becomes {} or lost | Preserved as RegExp |
| Circular references | Throws | Throws (same, but clearer error) |
const original = {
date: new Date('2024-01-01'),
set: new Set([1, 2, 2]),
big: BigInt(9007199254740991)
};
const jsonClone = JSON.parse(JSON.stringify(original));
console.log(typeof jsonClone.date); // 'string' — type is lost
console.log(jsonClone.big); // undefined — BigInt throws earlier
const properClone = structuredClone(original);
console.log(properClone.date instanceof Date); // true
console.log(properClone.big); // 9007199254740991n
For any data that includes Date, BigInt, Map, Set, or RegExp, structuredClone is the clear choice.
Browser and runtime support
structuredClone has broad support:
- Chrome / Edge: 98+
- Safari: 15.4+
- Firefox: 94+
- Node.js: 17.0.0+
- Deno: 1.21+
IE11 is not supported. If you need to support legacy environments, fall back to JSON.parse(JSON.stringify()) for simple data or use a library like Lodash’s cloneDeep.
When to use structuredClone
Reach for structuredClone when you need to:
- Pass complex data to a Web Worker or between windows
- Store an immutable snapshot of state without shared references
- Duplicate data before making transformations that should not affect the source
- Clone plain data objects that include
Date,Map,Set,RegExp, orBigInt
For class instances, circular references, or objects with methods, you still need a custom deep copy function or a library.
Why it fits modern apps
Most apps need copies that preserve type information. If state includes dates, maps, or typed arrays, JSON helpers force you to rebuild them by hand. structuredClone keeps the shape of the data intact, so the code that consumes the clone can stay simple. That makes it a good default when the value is already in a structured form and you only need a fresh copy.
Transfers for large payloads
When a buffer is only needed in one place, transfer it rather than copying it. That is especially useful for worker handoff, canvas data, and binary parsing, where the same bytes would otherwise be duplicated. The original reference becomes detached, which sounds harsh but is exactly what keeps the move cheap and predictable.
Choose the right boundary
Clone at the edge of an operation, not in the middle of a chain. If you clone too early, you create extra work and lose the chance to pass references through pure functions. If you clone too late, you risk accidental mutation leaking between steps. The best place is usually right before data crosses a thread, cache, or undo boundary.
Know the Limits
structuredClone is not a fix for every data model. Class instances still lose their prototype, functions still fail, and symbol keys still disappear. That is not a bug in your code; it is the algorithm telling you that the object is carrying behavior instead of just data. When that happens, model the value more explicitly or use a custom serializer.
Snapshotting State
Clone data when you need a clean snapshot that will not change behind your back. This is useful before optimistic updates, undo stacks, or handing work to another thread. The clone gives you a stable baseline, so later code can compare the old and new versions without wondering whether another part of the app already mutated the source.
Keep data and behavior separate
The algorithm is best when the value is mostly data. If an object depends on methods, prototypes, or hidden state, it may be better to remodel the data first and restore behavior later. That split keeps the clone process predictable and avoids bugs where a copied object looks correct but no longer acts like the original.
Prefer explicit models
If you find yourself cloning the same shape over and over, it may be a sign that the data model should be clearer. Small, explicit objects are easier to clone, easier to compare, and easier to send across thread boundaries. That also makes the code that creates them easier to test because the shape is obvious.
Keep the clone at the edge
Clone right before the data leaves a layer, not in the middle of a transformation. That keeps the working copy flexible while still giving you a safe snapshot when you need one. A tidy boundary like that keeps mutation bugs from spreading through the rest of the pipeline.
See Also
- JavaScript Symbols, Symbol as values vs Symbol as property keys
- JavaScript Promises in Depth, async patterns and data passing
- JavaScript WeakMap and WeakSet, working with weak references