Understanding StructuredClone: The Modern Way to Deep Copy In JavaScript
Updated on · 5 min read|
JavaScript developers are all too familiar with the frustration of dealing with reference types. Countless hours are spent debugging issues caused by unintentional object mutations - a common pain point in JavaScript development.
For years, creating deep copies of objects in JavaScript was unnecessarily difficult. While shallow copying with Object.assign()
or the spread operator is straightforward, deep copying nested structures required hacky workarounds or external dependencies. That's why the addition of structuredClone()
to the language was such a welcome improvement.
This article explores how structuredClone()
works, how it compares to other copying techniques, and the gotchas developers should know about before implementing it in their code.
The challenge of deep copying in JavaScript
The root of the problem lies in how JavaScript treats objects. When assigning an object to a variable, it doesn't create a copy but rather a reference to the original object. Changes to one reference affect all references pointing to the same object:
javascriptconst original = { name: "John", profile: { age: 30 } }; const copy = original; // Creates a reference, not a copy copy.profile.age = 31; console.log(original.profile.age); // Outputs: 31 - original was modified!
javascriptconst original = { name: "John", profile: { age: 30 } }; const copy = original; // Creates a reference, not a copy copy.profile.age = 31; console.log(original.profile.age); // Outputs: 31 - original was modified!
This issue can trip up even experienced developers. A common scenario is when modifying a copy of some state without affecting the original - something particularly important in state management libraries like Redux, or when working with immutable data patterns.
Traditional deep copying methods
Before structuredClone()
arrived, developers had to resort to various imperfect solutions. Here are the approaches most commonly used:
1. JSON parse/stringify method
The most widely used approach has been the JSON roundtrip hack:
javascriptconst original = { name: "John", profile: { age: 30 } }; const copy = JSON.parse(JSON.stringify(original)); copy.profile.age = 31; console.log(original.profile.age); // Still 30 - success!
javascriptconst original = { name: "John", profile: { age: 30 } }; const copy = JSON.parse(JSON.stringify(original)); copy.profile.age = 31; console.log(original.profile.age); // Still 30 - success!
This works for simple objects, but breaks down when dealing with complex data structures. The method fails when handling:
- Circular references (crashes with a circular structure error)
- Functions (they simply disappear)
undefined
values (also disappear)- Special objects like Maps and Sets (converted to empty objects)
- Date objects (converted to strings)
Production bugs often occur when dates in a cloned object suddenly behave like strings, making this approach too risky for general use.
2. Recursive cloning
A more robust approach is writing a custom recursive clone function:
javascriptfunction deepClone(obj) { if (obj === null || typeof obj !== "object") return obj; const copy = Array.isArray(obj) ? [] : {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { copy[key] = deepClone(obj[key]); } } return copy; }
javascriptfunction deepClone(obj) { if (obj === null || typeof obj !== "object") return obj; const copy = Array.isArray(obj) ? [] : {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { copy[key] = deepClone(obj[key]); } } return copy; }
This has better support for special cases but requires writing and maintaining custom code, and still doesn't handle all edge cases properly, e.g., circular references, built-in objects (Date, Map, Set), property descriptors, and Symbol keys.
3. Libraries
Eventually, most projects turn to libraries. Lodash's cloneDeep()
became the go-to solution:
javascriptimport { cloneDeep } from "lodash"; const copy = cloneDeep(original);
javascriptimport { cloneDeep } from "lodash"; const copy = cloneDeep(original);
While effective, it means adding an external dependency - you need to install the package.

Why structuredClone() is better
The implementation of structuredClone()
in browsers marked a significant improvement. It's part of the HTML standard and now available in all modern browsers and Node.js environments.
The API is refreshingly simple:
javascriptconst original = { name: "John", profile: { age: 30 }, dates: [new Date()], map: new Map([["key", "value"]]), }; // Modify the copy without affecting the original const copy = structuredClone(original); copy.profile.age = 31; console.log(original.profile.age); // Still 30
javascriptconst original = { name: "John", profile: { age: 30 }, dates: [new Date()], map: new Map([["key", "value"]]), }; // Modify the copy without affecting the original const copy = structuredClone(original); copy.profile.age = 31; console.log(original.profile.age); // Still 30
structuredClone()
has several important benefits:
1. Handles complex data structures
Unlike the JSON method, structuredClone()
correctly handles structures that previously caused headaches:
- Circular references work out of the box
- It preserves Date objects as actual dates
- Maps, Sets, RegExp objects, ArrayBuffers, and more are properly cloned
- Arrays and typed arrays maintain their proper types
Here's a simple example with circular references that would crash using JSON methods:
javascriptconst original = { name: "Circular Object" }; original.self = original; const copy = structuredClone(original); console.log(copy.self === copy); // true - circular reference maintained
javascriptconst original = { name: "Circular Object" }; original.self = original; const copy = structuredClone(original); console.log(copy.self === copy); // true - circular reference maintained
2. Native implementation
There's something satisfying about not needing external dependencies for basic operations. Being native to the language means:
- One less npm package to worry about
- Better performance in many cases
- Consistent behavior across environments
3. Performance
In testing, structuredClone()
can be faster than JSON methods for complex objects and objects with circular references, depending on data shape and JavaScript engine. The performance difference is particularly noticeable with large, deeply nested structures, though for small, purely-data objects, JSON methods might still be more efficient.
The limitations you should know
structuredClone()
isn't perfect. Several limitations must be considered:
1. Function handling
The biggest gotcha is with functions. They simply can't be cloned:
javascriptconst original = { name: "John", sayHello: function () { console.log("Hello!"); }, }; try { const copy = structuredClone(original); } catch (error) { console.error(error); // DataCloneError: Function object could not be cloned. }
javascriptconst original = { name: "John", sayHello: function () { console.log("Hello!"); }, }; try { const copy = structuredClone(original); } catch (error) { console.error(error); // DataCloneError: Function object could not be cloned. }
This restriction makes sense when understanding that structuredClone()
uses the same serialization algorithm as postMessage()
, but it can be a real obstacle when working with objects that mix data and behavior. For more nuanced approaches to debugging these issues, alternative techniques may be needed.
2. DOM nodes
Most live DOM nodes (Element, Text, etc.) cannot be cloned with structuredClone()
:
javascriptconst element = document.createElement("div"); try { const copy = structuredClone(element); } catch (error) { console.error(error); // DataCloneError: The object could not be cloned. }
javascriptconst element = document.createElement("div"); try { const copy = structuredClone(element); } catch (error) { console.error(error); // DataCloneError: The object could not be cloned. }
However, transferrable DOM-related objects like Blob, File, and ImageBitmap are supported and can be cloned properly.
3. Prototype chain and class instances
A subtler issue involves the prototype chain. When cloning class instances, you get plain objects without their methods:
javascriptclass Person { constructor(name) { this.name = name; } greet() { return `Hello, ${this.name}!`; } } const original = new Person("John"); const copy = structuredClone(original); console.log(copy.name); // "John" console.log(copy instanceof Person); // false console.log(copy.greet); // undefined
javascriptclass Person { constructor(name) { this.name = name; } greet() { return `Hello, ${this.name}!`; } } const original = new Person("John"); const copy = structuredClone(original); console.log(copy.name); // "John" console.log(copy instanceof Person); // false console.log(copy.greet); // undefined
Developers often need to implement custom clone methods in their classes to work around this limitation.
Note that this applies only to user-defined classes; built-in classes like Date, Map, and RegExp do maintain their prototypes when cloned.
4. Error objects
Error objects are actually cloneable with structuredClone()
. The message, name, cause, and enumerable own properties are preserved in the clone. However, the stack property is host-dependent and may be dropped during cloning, which means some debugging information could be lost.
javascriptconst originalError = new Error("Something went wrong"); const clonedError = structuredClone(originalError); console.log(clonedError instanceof Error); // true console.log(clonedError.message); // 'Something went wrong' // The stack trace might not be preserved
javascriptconst originalError = new Error("Something went wrong"); const clonedError = structuredClone(originalError); console.log(clonedError instanceof Error); // true console.log(clonedError.message); // 'Something went wrong' // The stack trace might not be preserved
5. Property descriptors
Property attributes like getters, setters, or other property descriptors aren't preserved in the clone, which can cause unexpected behavior when these features are relied upon.

When to use structuredClone()
Despite these limitations, structuredClone()
is particularly valuable when:
- Preserving complex data structures like nested application state
- Working with objects that might have circular references
- Creating snapshots of data from API responses for manipulation
- Implementing undo/redo functionality that requires state history
- Making independent copies of data for comparison or analysis
Practical tips from experience
Here are some useful approaches when working with structuredClone()
:
- Separate data from behavior: Keep pure data objects (which clone well) separate from objects with methods (which don't)
- Error handling matters: Always wrap clone operations in try/catch blocks in critical code paths
- Performance considerations: For small objects without special types, the JSON method might still be faster
- Class instance handling: For cloning class instances, consider implementing custom clone methods or using higher-order functions for transformation
Browser and Node.js support
structuredClone()
is available in:
- Chrome 98+
- Firefox 94+
- Safari 15.4+
- Edge 98+
- Node.js 17.0.0+ (as a global function)
- Node.js 16.17.0+ (LTS) as a global function
- Node.js 15.0.0 - 16.16.0 via
util.structuredClone()
For older environments, polyfills or traditional methods are needed.
Conclusion
Adding structuredClone()
to JavaScript was a significant quality-of-life improvement for developers. While it has limitations around functions and prototype chains, it elegantly handles most deep copying needs without external dependencies.
The next time you reach for JSON.parse/stringify or pull in Lodash just for deep cloning, consider whether structuredClone()
might be a better fit. For data-heavy applications that need to manipulate copies of complex objects, it has become the default choice for many developers.
References and resources
- MDN: structuredClone()
- HTML Living Standard: structuredClone
- MDN: The structured clone algorithm
- Can I Use: structuredClone
- MDN: Object.assign()
- MDN: JSON.stringify()
- MDN: JSON.parse()
- Lodash Documentation: cloneDeep
- The Most Common Mistakes When Using React: Pass by value vs. pass by reference
- Immutable Array Operations in JavaScript: Introducing toSorted, toSpliced, and toReversed
- Simplifying Code with Maps in JavaScript
- Beyond console.log: Debugging techniques in JavaScript
- JavaScript Higher-Order Functions Explained