What you'll be able to do
By the end of this page you should be able to spot shared-state bugs, avoid mutating objects you do not own, prefer new objects when updating fields, and know when in-place mutation is still fine for a small local script.
Previous post: primitives vs references.
Who this is for
- People who fixed a bug in one place and watched another screen break
- Beginners passing the same user object into two helpers
- Anyone who will later meet React state and wonders why “don’t mutate” is a rule
You can skip this if copy-on-write updates already feel natural. Come back when a “simple” obj.x = pollutes data you thought was private.
The bug shape
function promote(user) {
user.role = "admin"; // mutates caller's object
return user;
}
const current = { name: "Asha", role: "editor" };
promote(current);
console.log(current.role); // "admin" — caller changed too
Scenario A — one page, one object. Mutation seems fine until a second feature reads current.
Scenario B — “undo” or “previous.” You no longer have the old role; it was overwritten in place.
Prefer returning a new object
function promote(user) {
return { ...user, role: "admin" };
}
const current = { name: "Asha", role: "editor" };
const next = promote(current);
console.log(current.role); // "editor"
console.log(next.role); // "admin"
Scenario A — UI-friendly update. Old and new both available.
Scenario B — nested field. Spread is shallow — update nested pieces carefully (next post on deep copy). For flat fields, spread is enough.
When mutation is OK
Local-only data you created in the same function, a tight loop building one result object, or a tiny throwaway script can mutate in place if nothing else holds a reference.
Scenario A — accumulate into acc inside reduce. Common and fine when acc started as {} you own.
Scenario B — library or React props. Treat inputs as read-only; return new data.
Shared arrays, same story
function addTag(tags, tag) {
tags.push(tag); // shared mutation
return tags;
}
Prefer return tags.concat(tag) or return [...tags, tag] when the array might be shared.
A tiny practice file
Save as shared-state.html.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>shared state</title>
</head>
<body>
<button id="mutate">Mutating promote</button>
<button id="copy">Copy-on-write promote</button>
<pre id="out"></pre>
<script>
const out = document.getElementById("out");
document.getElementById("mutate").addEventListener("click", function () {
const current = { name: "Asha", role: "editor" };
function promote(user) {
user.role = "admin";
return user;
}
promote(current);
out.textContent = "after mutate, current.role=" + current.role;
});
document.getElementById("copy").addEventListener("click", function () {
const current = { name: "Asha", role: "editor" };
function promote(user) {
return { ...user, role: "admin" };
}
const next = promote(current);
out.textContent =
"current.role=" + current.role + " next.role=" + next.role;
});
</script>
</body>
</html>
Mistakes I see a lot
1. Mutating arguments “for convenience.”
2. Storing the same object in two lists. Edit one entry, both views change.
3. Logging after mutation and thinking you still have the old snapshot.
4. Deep nested edits with one spread. Nested objects remain shared.
5. Mixing mutate-in-place and copy-on-write in the same feature. Pick one style per layer.
6. Assuming const protects contents. It does not.
What to try before the next post
- Write mutating vs copy-on-write
promote. - Share one object in two variables; mutate; log both.
- Prefer
[...tags, t]overpushfor a shared list. - Build the tiny HTML file.
Next in this series: shallow vs deep copy — when spread is enough, and when structuredClone (or care) is needed.
Try this next outside the series
Objects and references click faster when you connect them to state, payloads, and database-shaped data.
- MERN MongoDB modeling — see objects turn into real documents and nested structures
- JSON Formatter — inspect deep objects before you mutate or copy them