Understanding StructuredClone: The Native Way to Deep Copy In JavaScript

Updated on · 4 min read
Understanding StructuredClone: The Native Way to Deep Copy In JavaScript

JavaScript developers struggle with reference types. Unintentional object mutations cause countless debugging hours - a persistent pain point in JavaScript development.

Creating deep copies of objects in JavaScript used to be unnecessarily difficult. While Object.assign() or the spread operator handle shallow copying well, deep copying nested structures required hacky workarounds or external libraries. The introduction of structuredClone() solved this problem elegantly.

This article explains how structuredClone() works, compares it to other copying techniques, and highlights important limitations you should know before using it.

This site is built on Next.js + Tailwind portfolio and blog template. Available on Gumroad.

Deep copying in JavaScript: the problem and traditional solutions

JavaScript treats objects as references. When you assign an object to a variable, you create a reference to the original object, not a copy. Changes to one reference affect all references pointing to the same object:

javascript
const 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!
javascript
const 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 behavior confuses even experienced developers. A common scenario requires modifying a copy of state without affecting the original - critical in Redux or when working with immutable data patterns.

Before structuredClone(), developers relied on these imperfect solutions:

The JSON roundtrip method

The most common approach used JSON serialization:

javascript
const 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!
javascript
const 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 fails with:

  • Circular references (crashes with a circular structure error)
  • Functions (they disappear)
  • undefined values (they disappear)
  • Maps and Sets (convert to empty objects)
  • Date objects (convert to strings)

Production bugs occur when dates in cloned objects behave like strings, making this approach risky for general use.

Custom recursive cloning and external libraries

A more robust approach involves writing a custom recursive clone function:

javascript
function 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; }
javascript
function 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 handles more cases but requires maintaining custom code and still fails with circular references, built-in objects (Date, Map, Set), property descriptors, and Symbol keys.

Most projects eventually use libraries. Lodash's cloneDeep() became the standard:

javascript
import { cloneDeep } from "lodash"; const copy = cloneDeep(original);
javascript
import { cloneDeep } from "lodash"; const copy = cloneDeep(original);

While effective, this requires adding an external dependency.

structuredClone(): benefits and capabilities

The implementation of structuredClone() in browsers significantly improved deep copying. It's part of the HTML standard and available in all modern browsers and Node.js.

The API is remarkably simple:

javascript
const original = { name: "John", profile: { age: 30 }, dates: [new Date()], map: new Map([["key", "value"]]), }; // Create a true deep copy const copy = structuredClone(original); copy.profile.age = 31; console.log(original.profile.age); // Still 30
javascript
const original = { name: "John", profile: { age: 30 }, dates: [new Date()], map: new Map([["key", "value"]]), }; // Create a true deep copy const copy = structuredClone(original); copy.profile.age = 31; console.log(original.profile.age); // Still 30

Unlike JSON methods, structuredClone() properly handles:

  • Circular references
  • Date objects (preserves them as actual dates)
  • Maps, Sets, RegExp objects, and ArrayBuffers
  • Arrays and typed arrays (maintains proper types)

Here's an example with circular references that would crash using JSON:

javascript
const original = { name: "Circular Object" }; original.self = original; const copy = structuredClone(original); console.log(copy.self === copy); // true - circular reference preserved
javascript
const original = { name: "Circular Object" }; original.self = original; const copy = structuredClone(original); console.log(copy.self === copy); // true - circular reference preserved

Being built into the language means no external dependencies, better performance in many cases, and consistent behavior across environments. structuredClone() often outperforms JSON methods for complex objects, especially those with circular references.

Limitations and practical usage

Despite its advantages, structuredClone() has several important limitations:

Function and DOM node limitations

The biggest limitation is with functions - they cannot be cloned:

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

Similarly, live DOM nodes cannot be cloned:

javascript
const element = document.createElement("div"); try { const copy = structuredClone(element); } catch (error) { console.error(error); // DataCloneError: The object could not be cloned. }
javascript
const element = document.createElement("div"); try { const copy = structuredClone(element); } catch (error) { console.error(error); // DataCloneError: The object could not be cloned. }

Class instances and property descriptors

When cloning class instances, you get plain objects without their methods:

javascript
class 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
javascript
class 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

Property attributes like getters, setters, and other property descriptors don't survive in the clone. Error objects can be cloned, but the stack property might be lost depending on the JavaScript environment.

When to use structuredClone() and browser support

Despite these limitations, structuredClone() excels when:

  1. You need to preserve complex data structures like nested application state
  2. Your objects might have circular references
  3. You need to create snapshots of API response data for manipulation
  4. You're implementing undo/redo functionality requiring state history

For best results:

  • Separate data from behavior, keeping pure data objects (which clone well) separate from objects with methods
  • Add error handling with try/catch blocks in critical code paths
  • Consider performance needs (JSON methods might still be faster for simple objects)
  • Implement custom clone methods for class instances

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, use polyfills or traditional methods.

Conclusion

The addition of structuredClone() to JavaScript significantly improved developer experience. While it can't handle functions or preserve prototype chains, it elegantly solves most deep copying needs without external dependencies.

The next time you reach for JSON.parse/stringify or add Lodash just for deep cloning, consider whether structuredClone() might work better. For data-heavy applications that manipulate copies of complex objects, it has become the preferred choice for many developers.

References and resources