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. 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.
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.
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
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 }
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.
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