Advertisement

You copy an object, change the copy, and the original changes too. You pass a list into a function, the function tidies it up, and now your caller's list is different. These moments feel like the language is haunted, but they all come from one idea: some values are copied when you assign them, and some are shared by reference.

Once you can tell which is which, a whole category of bugs stops being mysterious and becomes predictable. It is one of those fundamentals that pays back every day, in every language you touch.

Primitives copy, objects share

Simple values — numbers, booleans, short strings in most languages — are copied when you assign or pass them. Change the copy and the original is untouched, because they were never linked. This is why passing a number into a function and incrementing it inside does not change the caller's number.

Objects, arrays and other compound structures usually behave differently: the variable holds a reference — a pointer to the one underlying thing. Assigning it to another variable copies the pointer, not the contents, so both names now refer to the same object. Mutating through one name is visible through the other, because there is only one object.

Advertisement

The functions-mutate-their-arguments trap

This is why a function that "just sorts" or "just cleans" a list can quietly rewrite data its caller still relies on. The function received a reference to the same array, and sorting in place changed the shared thing. The fix is to decide deliberately: either document that the function mutates, or make a copy at the top and work on that.

Defaulting to non-mutating functions — take input, return a new value, leave the argument alone — removes an enormous amount of action-at-a-distance from a codebase. When a function must mutate for performance, making that explicit in its name and docs turns a hidden hazard into a clear contract.

Shallow copies are only skin deep

The usual "copy this object" tools produce a shallow copy: a fresh top-level object whose nested objects are still shared references to the originals. Change a nested field and both copies see it. For flat data that is fine; for nested data it reintroduces the exact bug you were trying to avoid.

When you genuinely need independence all the way down, reach for a deep copy — but know it is more expensive and can choke on cycles. Most of the time the cleaner answer is to avoid mutation in the first place, so the question of copying rarely comes up. Structure your data flow so shared references are read, not written.

Advertisement

Why primitives copy and objects share, and why that split feels arbitrary until it clicks

Most mainstream languages copy primitive values — numbers, booleans, and in many languages strings — by value on assignment or when passed to a function, while objects and arrays are copied by reference, meaning the variable actually holds a pointer to shared underlying data rather than an independent copy of it; this split can feel arbitrary to someone new to it, but it exists for a concrete efficiency reason — copying a large object or array on every single assignment or function call would be prohibitively expensive, while copying a small primitive value is essentially free, which is exactly why the language design favors reference semantics for the large, expensive-to-copy case and value semantics for the small, cheap one.

The function-parameter version of this bug: mutating an argument the caller did not expect to change

Passing an object into a function and mutating one of its properties inside that function mutates the caller's own original object too, since both the function's parameter and the caller's variable are simply two separate references to the exact same underlying object — a function that does this without the caller expecting it produces a class of bug that is often discovered much later, once the caller's own code behaves unexpectedly for reasons that trace back to a mutation buried inside a function call that looked, from the outside, like it should have been a read-only operation.

Advertisement

Comparing objects with `==` versus comparing their contents

Comparing two separately created objects with identical contents using an equality operator that checks reference identity returns false, since they are two distinct objects in memory even though their contents are identical — this trips up nearly everyone encountering it for the first time, expecting equality to mean 'the same data' rather than 'the same specific object,' and the fix, using a deep-equality comparison function or comparing specific fields directly rather than the whole object by reference, only makes sense once the value-versus-reference distinction described throughout this article is genuinely understood rather than worked around by trial and error.

Why this bug recurs across every language with reference semantics, in slightly different clothes

The specific syntax and exact rules differ between languages — Python's mutable default argument trap, Java's distinction between primitive and object types, JavaScript's array and object reference semantics — but the underlying mechanism causing each of these is the identical value-versus-reference split this article describes, which is worth recognizing explicitly, since understanding the general principle transfers directly across languages even when each one's specific manifestation of the bug looks superficially different at first glance.

Why deep cloning is the general fix, and why it is not free to apply everywhere

Creating a genuinely independent deep copy of an object, rather than merely a shallow one, guarantees that mutating the copy never affects the original at any nesting depth, which is the general-purpose fix for the shared-mutation bug this article describes — but deep cloning a large, deeply nested object on every single operation carries a real performance cost, which is exactly why the structural-sharing techniques and persistent data structures discussed in this library's mutability articles exist as a more efficient middle ground between always deep-cloning and never protecting against shared mutation at all.

Why this bug is disproportionately common in code that was recently converted from one paradigm to another

Code migrated from a style that assumed value semantics into a language or pattern using reference semantics, or the reverse, frequently carries over assumptions from its original context that no longer hold in the new one — a function ported from a value-semantics language into JavaScript, for instance, might assume passing an object around implicitly created a copy, when in the new context it does not, which is exactly the kind of assumption worth explicitly re-verifying during any cross-paradigm code migration rather than trusting that behavior transferred unchanged.

Why some languages let a type explicitly opt into value semantics for objects

Languages like Swift and Rust let a type declare itself as a value type (a struct) rather than a reference type (a class), which changes copy behavior at the type-definition level rather than leaving it as an implicit, easy-to-forget property of every individual object — deliberately choosing value semantics for a type specifically designed to avoid the shared-mutation risk this whole article describes is a design decision worth making explicitly wherever the language actually supports the choice.

Why a linter rule flagging suspicious mutation of function parameters is worth enabling

Several static analysis tools can flag a function that mutates one of its own object or array parameters, surfacing exactly the class of bug this article describes at review time rather than waiting for it to manifest as a confusing runtime surprise later — enabling this specific rule costs a brief initial cleanup pass and then quietly prevents a real, recurring category of mistake from being introduced again in the future.

Why explaining this to a beginner benefits from a physical, non-code analogy

Comparing a primitive value to writing a number on a sticky note handed directly to someone, and a reference to handing someone the address of a house rather than the house itself, gives a beginner a concrete mental model before any code syntax enters the picture at all — two people holding the same house address can both walk over and rearrange the same furniture, while two people each holding their own sticky note with the same number written on it are working with entirely separate, independent pieces of paper.

Why immutable data structures, covered elsewhere in this library, remove this bug at its root

Every fix discussed throughout this article works around the consequences of shared, mutable references, while adopting genuinely immutable data structures, discussed at length in this library's mutability articles, removes the root cause entirely — if nothing can ever be mutated in place at all, whether a variable holds a shared reference or an independent copy stops mattering, since neither one can ever be changed out from under the other in the first place.

Why understanding this distinction is foundational to reasoning correctly about concurrency

Every concurrency bug involving two threads unexpectedly interfering with each other's data ultimately traces back to a shared reference neither thread realized the other also held — the value-versus-reference mental model this article builds is not merely a single-threaded correctness concern, it is the conceptual foundation for reasoning correctly about concurrent code at all, since concurrency bugs are, structurally, exactly this same shared-mutation problem playing out across multiple simultaneously executing threads rather than across sequential function calls.

Why this article's lesson is ultimately about making an implicit assumption explicit

Every bug covered throughout this article traces back to the same root cause: an implicit, unexamined assumption about whether an assignment or function call created an independent copy or merely a second reference to the same underlying data — the actual fix is not memorizing every language's specific copy rules in isolation, it is building the habit of making that assumption explicit and checking it deliberately, every time, rather than trusting instinct alone in a domain where instinct is demonstrably unreliable.

Why this bug's persistence across decades of language design says something about its genuine difficulty

Despite being one of the most thoroughly documented categories of bug in all of software development, value-versus-reference confusion continues to trip up developers at every experience level across every generation of mainstream languages, which suggests the difficulty is not a documentation gap or a lack of awareness, but a genuine mismatch between how humans naturally think about copying something and how computers actually represent that operation underneath.

Why teaching this concept early, before bad habits form, pays off disproportionately

A developer who internalizes the value-versus-reference distinction clearly during their earliest exposure to programming carries that clarity into every subsequent language they learn, while one who first develops an incorrect intuition has to actively unlearn it later — this is exactly why the concept deserves deliberate, explicit early teaching rather than being left to be absorbed incidentally, and imperfectly, through trial and error over the course of a career.