How to Clone a JavaScript Object without Errors
Learn safe ways to clone JavaScript objects: shallow copy with spread and Object.assign, deep copy with structuredClone, and how to avoid the shared reference trap.
Cloning a JavaScript object means creating a new object with the same properties and values as the original, but stored at a different location in memory. The goal is independence: changing the clone should not affect the original.
The challenge is that JavaScript objects can contain nested objects, arrays, dates, and other non-primitive values. Choosing the wrong cloning method leaves you with shared references and hard-to-find bugs.
Why Direct Assignment Does Not Work
A common first attempt at cloning is simply assigning the object to a new variable. That does not create a second object at all. It copies the reference, so both variables still point to the exact same object in memory:
const original = { score: 100 };
const copy = original;
copy.score = 0;
console.log(original.score); // 0 (changed through copy!)Both original and copy point to the same object in memory. This is not a clone; it is an alias.
Shallow Copy with the Spread Operator
The spread operator copies all own enumerable properties into a new object:
const original = { name: "Project A", budget: 5000 };
const clone = { ...original };
clone.budget = 6000;
console.log(original.budget); // 5000 (unchanged)
console.log(clone.budget); // 6000For flat objects where every value is a primitive (string, number, boolean, null, undefined), the spread operator gives you a fully independent copy.
The problem appears when values are objects or arrays:
const original = {
name: "Project A",
settings: { theme: "dark", notifications: true }
};
const clone = { ...original };
clone.settings.theme = "light";
console.log(original.settings.theme); // "light" (shared reference!)The top-level name is independent, but settings is a reference to the same nested object. Changing it through the clone changes it everywhere.
Shallow Copy with Object.assign()
Object.assign() behaves the same way as spread for cloning:
const original = { x: 10, y: 20 };
const clone = Object.assign({}, original);
clone.x = 99;
console.log(original.x); // 10 (independent)Like spread, it creates a shallow copy. Nested objects are shared. For more on Object.assign, see the article on /javascript/how-to-use-object-assign-in-javascript-properly.
Deep Copy with structuredClone()
structuredClone() is the modern, built-in way to create a deep copy. It recursively copies all nested values, including objects, arrays, Dates, Maps, Sets, ArrayBuffers, and more:
const original = {
created: new Date("2026-01-15"),
items: ["apple", "banana"],
metadata: { version: 3 }
};
const clone = structuredClone(original);
clone.items.push("orange");
console.log(original.items); // ["apple", "banana"] (unchanged)Pushing to the clone's items array never touches the original, because structuredClone copied the array itself instead of just a reference to it. The same holds for the nested metadata object and the Date instance:
clone.metadata.version = 4;
console.log(original.metadata.version); // 3 (unchanged)
console.log(original.created.getFullYear()); // 2026 (unchanged)Every level is independent. structuredClone() is supported in all modern browsers and Node.js 17+. It handles most built-in types correctly.
What structuredClone cannot clone:
| Type | Behavior |
|---|---|
| Functions | Throws a DataCloneError |
| DOM nodes | Throws a DataCloneError |
| Class instances with methods | Methods are lost |
| Symbols | Throws a DataCloneError |
| WeakMap / WeakSet | Throws a DataCloneError |
For plain data objects, arrays, Dates, Maps, and Sets, structuredClone() is the best choice.
Deep Copy with JSON Methods
Before structuredClone(), the common deep-copy trick was JSON.parse(JSON.stringify(obj)):
const original = { name: "Doc", pages: 42 };
const clone = JSON.parse(JSON.stringify(original));
clone.pages = 99;
console.log(original.pages); // 42 (independent)This works for simple data, but converting an object to a JSON string and back silently drops or corrupts several value types along the way. Dates and functions are the two most common surprises:
const original = { created: new Date(), callback: () => "hello" };
const clone = JSON.parse(JSON.stringify(original));
console.log(clone.created); // "2026-07-15T..." (string, not Date)
console.log(clone.callback); // undefined (function lost)undefined properties and NaN do not survive the round trip either, since neither one has a valid representation in JSON:
const data = { id: undefined, count: NaN };
const clone = JSON.parse(JSON.stringify(data));
console.log(clone.id); // undefined (property removed entirely)
console.log(clone.count); // null (NaN becomes null)Use JSON methods only when you are certain the object contains only strings, numbers, booleans, arrays, and plain nested objects. For anything else, use structuredClone().
Quick Decision Guide
| Your data contains | Use |
|---|---|
| Only primitives (strings, numbers, booleans) | Spread { ...obj } |
| Nested objects, arrays, Dates | structuredClone(obj) |
| Functions, class instances, DOM nodes | Manual copy or a library |
| Unknown shape from an API | structuredClone(obj) (safest) |
Common Mistake: Accidental Mutation After a Shallow Copy
The most frequent cloning bug is using spread on an object with nested arrays and then pushing or splicing, expecting the original to be safe:
const template = { tags: ["js", "web"] };
const copy = { ...template };
copy.tags.push("css");
console.log(template.tags); // ["js", "web", "css"] (mutated!)The tags array is shared. If you need to modify nested arrays or objects independently, use structuredClone() or spread at every level manually. For a closer look at that manual spreading technique, see the article on /javascript/copying-nested-objects-with-the-js-spread-operator.
Rune AI
Key Insights
- Shallow copy (spread or Object.assign) copies only top-level properties. Nested objects are shared.
- Deep copy with structuredClone() recursively copies all levels and handles most built-in types.
- JSON.parse(JSON.stringify()) works for simple data but loses functions, Dates, and special objects.
- Always consider whether you need a shallow or deep copy before choosing a method.
- structuredClone() is available in all modern browsers and Node.js 17+.
Frequently Asked Questions
What is the difference between a shallow copy and a deep copy?
Does the spread operator create a deep copy?
Can I use JSON.parse(JSON.stringify(obj)) to deep clone?
Conclusion
Cloning an object correctly depends on how deep you need to go. For flat objects with only primitives, the spread operator is enough. For nested data, use structuredClone() for a proper deep copy that handles most JavaScript types. Avoid the shared reference trap by picking the right tool for your data shape.
More in this topic
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.
How to Loop Through Arrays Using JS for Loops Guide
Looping through arrays is the most practical use of JavaScript for loops. Learn how to access each element by index, avoid off-by-one errors, and choose between for, for...of, and forEach.