What Makes Rust’s Memory Model Different

The aspect of Rust that feels most intimidating is also the one that sets it apart from virtually every other mainstream language. Understanding it requires a shift in how you think about passing data around a program.

On the surface, Rust syntax is familiar. A function that prints a number looks like C:

fn show(n: i64) { println!("n = {n}"); }

Calling it looks like any C-family language—parentheses, curly braces, the works:

Cool bear

Even the string interpolation, which uses a format string rather than the C-style concatenation you might expect, doesn’t feel too foreign:

fn show(n: i64) { println!("n = {n}"); } fn main() { let n = 42; show(n); }

Value Semantics and the Copy Trait

Passing the same variable to a function twice works fine when that variable holds a number:

fn show(n: i64) { println!("n = {n}"); } fn main() { let n = 42; show(n); show(n); }

Switch the type to a String, though, and the same program stops compiling:

fn show(s: String) { println!("s = {s}"); } fn main() { let s = String::from("hiya"); show(s); show(s); }

The errors from Rust’s compiler are famously instructive. Where other toolchains produce noise to be ignored, Rust’s diagnostics walk you through its rules:

rust-is-hard on  main [?] is 📦 v0.1.0 via 🦀 v1.79.0 cargo c -q error[E0382]: use of moved value: `s` --> src/main.rs:8:10 | 6 | let s = String::from("hiya"); | - move occurs because `s` has type `String`, which does not implement the `Copy` trait 7 | show(s); | - value moved here 8 | show(s); | ^ value used here after move |

The lesson here is the Copy trait. An i64 implements Copy because duplicating an integer between registers is essentially free—exactly the kind of operation hardware is built for:

A String, by contrast, does not implement Copy. It’s a handle to a valid UTF-8 sequence living on the heap, a region managed by an allocator that tracks every memory reservation:

Making a second copy of a String means asking the allocator for more space. Frequently it can reuse recently freed memory, but in the worst case that request travels all the way down to the kernel to map additional pages:

That variable cost is why high-performance software carefully avoids allocation at critical times—real-time audio code won’t allocate on the audio thread, and games minimize it to avoid dropped frames. Rust embraces that reality by requiring you to be explicit: accepting a potentially expensive heap allocation means writing .clone().

note: consider changing this parameter type in function `show` to borrow instead if owning the value isn't necessary --> src/main.rs:1:12 | 1 | fn show(s: String) { | ---- ^^^^^^ this parameter takes ownership of the value | | | in this function help: consider cloning the value if the performance cost is acceptable | 7 | show(s.clone()); | ++++++++ For more information about this error, try `rustc --explain E0382`. error: could not compile `rust-is-hard` (bin "rust-is-hard") due to 1 previous error

Adding that call makes the earlier program compile and run:

fn show(s: String) { println!("s = {s}"); } fn main() { let s = String::from("hiya"); show(s.clone()); show(s); }

Cloning vs. Borrowing

The compiler’s other suggestion steers you toward passing the string by reference, or “borrowing” it. For read-only use, that works just as well:

// note: taking `&String` is needlessly restrictive, but one thing at a time. fn show(s: &String) { println!("s = {s}"); } fn main() { let s = String::from("hiya"); show(&s); show(&s); }

Which of these suggestions you pick only matters if you habitually think about memory management—a habit built in languages without garbage collection, like C and C++. But that knowledge transfers even if you arrive from JavaScript or Go, where memory safety normally isn’t your problem:

Amos

The String type knows exactly when the caller no longer needs the data being passed—and whether a reference will outlive the function call. That precision is what makes “borrow” such a useful mental model.

The Other Side: Higher-Level Languages

The gap between those two suggestions—clone or borrow—only becomes meaningful when you understand memory. But consider what happens in languages where you never manage memory manually, and how they handle the same questions: “Is this function allowed to change what I passed it?” and “Is it going to be able to?”

JavaScript’s Semantics

JavaScript has no explicit concept of “by value” or “by reference.” The behavior is implicit in the types involved. Numbers, being primitives, are passed by value—so this function receives two distinct copies of s, and both modifications vanish after the call ends:

function inc(s) { s += 1; } let s = 0; console.log(s); inc(s); console.log(s);

The result is that the original value prints as zero, twice:

To let a function mutate something, you’d wrap it in an object and pass that object:

function inc(o) { o.s += 1; } let o = { s: 0 }; console.log(o); inc(o); console.log(o);

That shares the mutation outward, because the object was passed by reference—the function modified the same underlying piece of memory that the caller reads later:

If you want the opposite—to pass an object but guarantee the function can’t modify it—you quickly run into options that aren’t very good. You could clone the object before handing it over:

let bad_deep_clone = (o) => JSON.parse(JSON.stringify(o)); function inc(o) { o.s += 1; } let o = { s: 0 }; console.log(o); inc(bad_deep_clone(o)); console.log(o);

The clone protects the original, but then your API has to define a clone step for every input you want to guard, just to express a basic guarantee about read-only access to an internal value:

Amos

Alternatively, you could freeze the object. That blocks changes to existing properties as well as adding new ones:

function inc(o) { o.s += 1; } let o = { s: 0 }; console.log(o); inc(Object.freeze(o)); console.log(o);

But freezing is permanent from there onward. In non-strict mode, the normal state for JavaScript unless you opt in, violations fail silently rather than throwing errors; code that works in strict mode is stricter about never touching the frozen thing after the fact:

The lack of guidance is why code like that clone snippet appears in production at all. Freezing an object is a global system call that permanently changes it—perhaps it protects you in the moment, but it disables a data structure for the rest of the application.

Cool bear

JavaScript makes a read-only guarantee impossibly specific, and gives you near-infinite ways to represent copy-vs-reference behavior without explicit words for it.

Go’s Approach

Go occupies a middle ground. It doesn’t carry JavaScript’s ambiguity, but its rules also fail to communicate intent through a function signature—whether the author intended the function to mutate its inputs.

Integers are passed by value; calling a function that adds one to a parameter leaves the caller unaffected, and the program prints zero twice:

package main import ( "log" ) func inc(i int) { i++ } func main() { log.SetFlags(0) i := 0 log.Println(i) inc(i) log.Println(i) }

The incremental function, both as written above and as the reference that failed to change anything:

A struct parameter is also by value. In Go, passing a struct means handing the callee a whole extra copy to work on—modifying that copy leaves the original struct unchanged:

package main import ( "log" ) type O struct { i int } func inc(o O) { o.i++ } func main() { log.SetFlags(0) o := O{i: 0} log.Printf("%v", o) inc(o) log.Printf("%v", o) }

It now accumulates an increment but throws it away when the call returns. This happens even if the struct is large—structures with many fields are still copied wholesale at every function boundary:

To actually change the caller’s struct, you must take its address. Inside the function signature, you express that fact with a pointer type; inside the body you dereference with the star operator. Now the program prints zero and one, because the pointer carried the original’s address:

package main import ( "log" ) type O struct { i int } func inc(o *O) { o.i++ } func main() { log.SetFlags(0) o := O{i: 0} log.Printf("%v", o) inc(&o) log.Printf("%v", o) }

That works—but it communicates only the fact that mutation happens, not that you want to prevent it when only a pointer is available:

Say someExternalFunc accepts a pointer. You cannot know from its signature whether it rewrites your data merely by looking at the body; keeping assumptions requires cloning defensively before calling:

package main import ( "log" ) type O struct { i int } func someExternalFunc(o *O) { // (this function is from an external library, all we know // is its type signature) } func main() { log.SetFlags(0) var o *O o = &O{i: 0} // how can we prevent `someExternalFunc` from mutating o? someExternalFunc(o) }

In Go, the style is pragmatically clear if semantically still implicit:

func main() { log.SetFlags(0) var o *O o = &O{i: 0} // Q: how can we prevent `someExternalFunc` from mutating o? // A: by making a copy of it { o2 := *o someExternalFunc(&o2) } }

Instead of returning the pointer that someone might hope to mutate later, you can return the dereferenced original—sharing an indefinite copy is safer at boundary crossings:

The deep-clone problem

JavaScript’s bad_deep_clone relies on JSON.parse for a reason: a true deep clone must copy not just an object but any objects it references, recursively. The object spread operator can’t do that. It produces only a shallow copy, so mutations can still reach the original object’s nested fields.

let shallow_clone = (o) => ({ ...o });

That shallow copy quickly reveals its limits: you can still change parts of the original.

The output is zero and one, not zero and zero.

rust-is-hard on  main [?] is 📦 v0.1.0 via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 node main.mjs { o: { s: 0 } } { o: { s: 1 } }

Writing a reliable deep clone is genuinely hard. What about cycles — s containing o containing s? A naive recursive clone will keep calling itself until the stack overflows. The JSON round-trip approach is a poor substitute: it chokes on dates and custom types, but at least it detects cycles.

let o = {}; o.s = { o }; // shorthand for { o: o } console.log(JSON.stringify(o));

Go has exactly the same issue. Dereferencing a struct with * only makes a shallow copy.

package main import ( "log" ) type Outer struct { inner *Inner } type Inner struct { i int } func inc(o *Outer) { o.inner.i += 1 } func main() { o := &Outer{inner: &Inner{i: 0}} log.Println(o.inner.i) { o2 := *o inc(&o2) } log.Println(o.inner.i) }

The output is again zero and one: the outer struct is copied, but the inner struct behind it is shared, not cloned.

As of Go 1.18, clone functions can be generic and return the type you pass in instead of an empty interface, but the problem hasn’t disappeared. A look at the go-clone README is enough to discourage hand-rolling: its authors document special handling for reference cycles (only addressed by a separate clone.Slowly method), arenas from Go 1.20, scalar-like pointer types such as time.Time, enum-like pointers such as elliptic.Curve, uncopyable values like sync.Mutex, atomic pointers, and more.

Const is a suggestion

Garbage-collected languages aren’t the only ones with this gap. In C and C++, a function taking a reference to const S still can’t be trusted. const_cast removes the constness and lets you write to memory that was declared immutable.

#include <iostream> struct S { int i; }; void pinky_promise_i_wont_mutate_u(const S& s) { S *s2 = const_cast<S*>(&s); s2->i += 1; } int main() { S s = {0}; std::cout << s.i << std::endl; pinky_promise_i_wont_mutate_u(s); std::cout << s.i << std::endl; }
rust-is-hard on  main is 📦 v0.1.0 via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 clang++ -Wall -Wpedantic main.cc -o main && ./main 0 1

C has the same escape hatch: a pointer to const S loses its protection with a single cast.

#include <stdio.h> struct S { int i; }; void pinky_promise_i_wont_mutate_u(const struct S* s) { struct S *s2 = (struct S*)s; s2->i += 1; } int main(void) { struct S s = {0}; printf("%d\n", s.i); pinky_promise_i_wont_mutate_u(&s); printf("%d\n", s.i); return 0; }
rust-is-hard on  main [?] is 📦 v0.1.0 via C v16.0.0-clang via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 clang -Wall -Wpedantic main.c -o main && ./main 0 1

Adding more const qualifiers only prevents reassignment of the pointer variable itself — it does nothing about mutating the pointee’s fields.

rust-is-hard on  main [?] is 📦 v0.1.0 via C v16.0.0-clang via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 clang -Wall -Wpedantic main.c -o main && ./main 0 1

Neither language can win this game because both are inherently unsafe at the core. Piling on static analysis on top of a lax foundation is whack-a-mole: squash one bug, another surfaces. The engineers maintaining existing C and C++ codebases deserve real credit, but these languages are best treated like asbestos — no new construction, and careful, gradual removal from existing infrastructure.

Ownership as a language feature

The contrast with Rust is stark. Consider this: calling show twice on the same value fails to compile.

fn show(s: String) { println!("s = {s}"); } fn main() { let s = String::from("hiya"); show(s); show(s); }

That error isn’t pedantry. Make the example concrete — pass a database connection, not a String:

struct Conn {} fn close(_c: Conn) { // TODO: free resources, etc. } fn main() { let conn = Conn {}; close(conn); close(conn); }

The signature of close tells the compiler, and the reader, that the argument is consumed. The first line of main owns conn. The second line moves it into close. The third line — using conn again — is a compile error because the value is gone.

That’s one way Rust lets you encode intent through types. The underlying concept has a long pedigree: Philip Wadler’s 1990 paper “Linear types can change the world!” anticipated it, and it’s been explored in linear Lisp, uniqueness types in Clean, single assignment C, Hermes, Cyclone, and limited types in Ada. Rust is arguably the first to take the idea mainstream. It likely won’t be the last. Circle, a C++ extension exploring similar territory, is worth a mention even if most of the C++ community ignores it.

Immutability without const

Ownership is only half the story. Rust also lets you borrow values without transferring ownership, passing a shared reference when the callee only needs to read:

struct Conn { name: String, } fn get_conn_name(c: &Conn) -> &str { &c.name } fn main() { let conn = Conn { name: String::from("foobar"), }; println!("{}", get_conn_name(&conn)); println!("{}", get_conn_name(&conn)); }

There’s no const keyword making this happen — Rust’s const is for compile-time evaluation and globals, not immutability. And yet, mutation through the shared reference is impossible. The compiler rejects an attempt to assign to c.name because c is a shared reference and the data behind it may not be written.

rust-is-hard on  main [!⇡] is 📦 v0.1.0 via C v16.0.0-clang via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 cargo c -q error[E0594]: cannot assign to `c.name`, which is behind a `&` reference --> src/main.rs:6:5 | 6 | c.name = String::from("ahAH!"); | ^^^^^^ `c` is a `&` reference, so the data it refers to cannot be written | help: consider changing this to be a mutable reference | 5 | fn get_conn_name(c: &mut Conn) -> &str { | +++ For more information about this error, try `rustc --explain E0594`. error: could not compile `rust-is-hard` (bin "rust-is-hard") due to 1 previous error

Nesting doesn’t provide an escape, unlike in the Go, JavaScript, and C/C++ examples. The compiler sees through all the types and layers, and it can verify whether a write is happening through a shared reference. It is against the rules, and it doesn’t compile.

Amos

Unsafe Code Isn’t "Unsafe" — It’s Unchecked

What happens when we try to sidestep safety measures? In C and C++, casting away const is legal and common. Rust allows the Rust equivalent — casting a const pointer to a mut pointer — even in safe code. That cast compiles fine.

The trouble starts when we actually use that mutable pointer. The moment we dereference it and attempt to assign a field, the compiler steps in with a pointed warning. The error message spells it out: raw pointers may be null, dangling, or unaligned. They can violate aliasing rules and cause data races. All of these are undefined behavior.

To proceed, we have to explicitly acknowledge that warning and wrap our logic in an unsafe block. Inside that block, the rest of Rust's safety rules still hold — no writing past the end of a Vec, for instance — but we're now permitted to do riskier things, like dereference raw pointers and call other unsafe functions.

struct Conn { name: String, } fn get_conn_name(c: &Conn) -> &str { // this is a terrible idea, for demonstration purposes: let s = c as *const Conn as *mut Conn; (*s).name = String::from("ahAH!"); &c.name } fn main() { let conn = Conn { name: String::from("foobar"), }; println!("{}", get_conn_name(&conn)); println!("{}", get_conn_name(&conn)); }
rust-is-hard on  main [+⇡] is 📦 v0.1.0 via C v16.0.0-clang via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 cargo c -q error[E0133]: dereference of raw pointer is unsafe and requires unsafe function or block --> src/main.rs:7:5 | 7 | (*s).name = String::from("ahAH!"); | ^^^^ dereference of raw pointer | = note: raw pointers may be null, dangling or unaligned; they can violate aliasing rules and cause data races: all of these are undefined behavior For more information about this error, try `rustc --explain E0133`. error: could not compile `rust-is-hard` (bin "rust-is-hard") due to 1 previous error

Once wrapped, the code compiles and runs. At least, it ran once — on my machine, with a specific Rust version, in debug mode. With cargo run --release, however, everything broke. The error came not from Rust's own checks but from macOS's system memory allocator, which caught us passing something to its free function that wasn't currently allocated. We got lucky the allocator even asserted; a segmentation fault, bus error, or silent memory corruption were all plausible outcomes.

struct Conn { name: String, } fn get_conn_name(c: &Conn) -> &str { let s = c as *const Conn as *mut Conn; unsafe { (*s).name = String::from("ahAH!"); } &c.name } fn main() { let conn = Conn { name: String::from("foobar"), }; println!("{}", get_conn_name(&conn)); println!("{}", get_conn_name(&conn)); }
rust-is-hard on  main [!+⇡] is 📦 v0.1.0 via C v16.0.0-clang via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 cargo r Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/rust-is-hard` ahAH! ahAH!
rust-is-hard on  main [!+⇡] is 📦 v0.1.0 via C v16.0.0-clang via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 cargo run --release Finished `release` profile [optimized] target(s) in 0.00s Running `target/release/rust-is-hard` 0ßL rust-is-hard(11789,0x1e72e1500) malloc: *** error for object 0x60000219c030: pointer being freed was not allocated rust-is-hard(11789,0x1e72e1500) malloc: *** set a breakpoint in malloc_error_break to debug zsh: abort cargo run --release

The root cause is straightforward: we promised to uphold Rust's invariants, then broke that promise by invoking undefined behavior. The compiler made optimizations that were perfectly legal given our guarantee — and when we reneged, it blew up in our faces.

A Finite List of Bad Moves

The Rustonomicon makes clear that Rust's undefined behavior is far more limited in scope than C's. The core language is concerned with preventing just six things:

  • Dereferencing (using the * operator on) dangling or unaligned pointers
  • Breaking pointer aliasing rules
  • Calling a function with the wrong call ABI, or unwinding with the wrong unwind ABI
  • Causing a data race
  • Executing code compiled with target features the current thread doesn't support
  • Producing invalid values

The Reference's "Behavior considered undefined" page offers further specifics. That doesn't mean unsafe Rust is easy — quite the opposite. Every piece of safe Rust code relies on a foundation of correctly written unsafe code, and the compiler can't check that foundation itself.

This is where miri comes in. A separate official Rust project, miri runs your program in an interpreter that actively detects undefined behavior. Running it via cargo +nightly miri run immediately flagged our bug: we held a "shared" reference (&Conn) and created an "exclusive" reference from it to do the write. That's a direct violation of the Rule of Two — shared references make their referent immutable, period.

Cool bear

The bytes pointed to by a shared reference, including transitively through other references (both shared and mutable) and Boxes, are immutable; transitivity includes those references stored in fields of compound types.

— Rust Reference, "Behavior considered undefined"

rust-is-hard on  main [!+⇡] is 📦 v0.1.0 via C v16.0.0-clang via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 cargo +nightly miri run --quiet error: Undefined Behavior: trying to retag from <2975> for Unique permission at alloc1339[0x0], but that tag only grants SharedReadOnly permission for this location --> /Users/amos/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:542:1 | 542 | pub unsafe fn drop_in_place<T: ?Sized>(to_drop: *mut T) { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | | | trying to retag from <2975> for Unique permission at alloc1339[0x0], but that tag only grants SharedReadOnly permission for this location | this error occurs as part of retag at alloc1339[0x0..0x18] | = help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental = help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information help: <2975> was created by a SharedReadOnly retag at offsets [0x0..0x18] --> src/main.rs:6:13 | 6 | let s = c as *const Conn as *mut Conn; | ^ = note: BACKTRACE (of the first span): = note: inside `std::ptr::drop_in_place::<std::string::String> - shim(Some(std::string::String))` at /Users/amos/.rustup/toolchains/nightly-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:542:1: 542:56 note: inside `get_conn_name` --> src/main.rs:8:9 | 8 | (*s).name = String::from("ahAH!"); | ^^^^^^^^^ note: inside `main` --> src/main.rs:17:20 | 17 | println!("{}", get_conn_name(&conn)); | ^^^^^^^^^^^^^^^^^^^^ note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace error: aborting due to 1 previous error

Rewriting With Mutable Borrows

Fixing the code is straightforward once miri shows us the way. Change the signature of get_conn_name to take an exclusive reference — marked with mut — and the first unsafe cast disappears:

fn get_conn_name(c: &mut Conn) -> &str { let s = c as *const Conn as *mut Conn; unsafe { (*s).name = String::from("ahAH!"); } &c.name }
fn get_conn_name(c: &mut Conn) -> &str { let s = c as *mut Conn; unsafe { (*s).name = String::from("ahAH!"); } &c.name }

Back at the callsite, conn must become a mutable binding (let mut) and we pass it with a mutable borrow (&mut conn):

fn main() { let mut conn = Conn { name: String::from("foobar"), }; println!("{}", get_conn_name(&mut conn)); println!("{}", get_conn_name(&mut conn)); }

This corrected version runs fine in both debug and release modes, and this time miri gives its seal of approval. The unsafe block remains, but it no longer invokes undefined behavior — through careful reasoning and miri's assistance, the program is actually memory-safe.

rust-is-hard on  main [!+⇡] is 📦 v0.1.0 via C v16.0.0-clang via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 cargo +nightly miri run --quiet ahAH! ahAH!

Why Not Just Remove unsafe Altogether?

In this particular case, the unsafe block turns out to be entirely unnecessary. The same effect is achievable in safe Rust — the entire point of exclusive references is to allow mutation:

struct Conn { name: String, } fn get_conn_name(c: &mut Conn) -> &str { c.name = String::from("ahAH!"); &c.name } fn main() { let mut conn = Conn { name: String::from("foobar"), }; println!("{}", get_conn_name(&mut conn)); println!("{}", get_conn_name(&mut conn)); }

That's why some argue the unsafe keyword is a misnomer. Code inside an unsafe block isn't inherently dangerous; it's code the compiler cannot verify. Better names might be unchecked, yolo, or hold_my_beer — though renaming it now would be quite a challenge.

Writing unsafe code is harder, riskier, and demands careful human review. Static analysis of unsafe code is still a work in progress. Even major projects struggle: tokio, for example, enables special codepaths specifically so miri can analyze what it's doing. The guiding principle remains simple: if you can write it in safe Rust, you should.

The Core Trade-Off

Ownership in Rust eliminates entire bug categories — double-closing a connection, for example, is simply impossible with the API shown below, no matter how the code is written:

struct Conn {} fn close(_c: Conn) { // TODO: free resources, etc. } fn main() { let conn = Conn {}; close(conn); close(conn); }

Beyond ownership, Rust builds on the concepts of borrowing and mutable borrowing, which yield shared and exclusive references respectively. The central rule that ties these together is "Aliasing XOR Mutability" (AXM): you can have multiple references to the same value, or you can mutate it, but never both at the same moment.

Cool bear

This constraint rules out data races at compile time. Several mutable references to one value could be handed to different threads, letting them modify the same memory concurrently — a recipe for corruption that attackers can exploit. This is not a theoretical concern; the same way we know what happens when seatbelts aren't worn, we know how buffer overflows lead to server break-ins.

struct Conn {} fn main() { let conn = Conn {}; let mut v = vec![]; v.push(&conn); v.push(&conn); v.push(&conn); println!("{}", v.len()) }

That code is silly but valid. The next example, however, is rejected:

struct Conn {} fn main() { let mut conn = Conn {}; let mut v = vec![]; v.push(&mut conn); v.push(&mut conn); v.push(&mut conn); println!("{}", v.len()) }
rust-is-hard on  main [!+⇡] is 📦 v0.1.0 via C v16.0.0-clang via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 cargo r -q error[E0499]: cannot borrow `conn` as mutable more than once at a time --> src/main.rs:7:12 | 6 | v.push(&mut conn); | --------- first mutable borrow occurs here 7 | v.push(&mut conn); | ^^^^^^^^^ second mutable borrow occurs here 8 | v.push(&mut conn); | - first borrow later used here (etc.)

What about a case where the code is technically correct but subtle? Here the borrow checker refuses to accept it because it cannot prove that calling clear ends the earlier mutable borrow:

struct Conn {} fn main() { let mut conn = Conn {}; let mut v = vec![]; v.push(&mut conn); v.clear(); v.push(&mut conn); v.clear(); v.push(&mut conn); v.clear(); println!("{}", v.len()) }
rust-is-hard on  main [!+⇡] is 📦 v0.1.0 via C v16.0.0-clang via 🐹 v1.22.5 via  v22.4.1 via 🦀 v1.79.0 cargo r -q error[E0499]: cannot borrow `conn` as mutable more than once at a time --> src/main.rs:8:12 | 6 | v.push(&mut conn); | --------- first mutable borrow occurs here 7 | v.clear(); 8 | v.push(&mut conn); | ^^^^^^^^^ second mutable borrow occurs here 9 | v.clear(); | - first borrow later used here (cut.)

Doing so would require the compiler to track the lifetimes of every element ever inserted into the Vec at compile time — an enormous limitation on expressiveness. If the mutable reference is instead returned by popping it out of the vector, the code compiles fine and can be pushed back in repeatedly:

Amos
struct Conn {} fn main() { let mut conn = Conn {}; let mut v = vec![]; v.push(&mut conn); let conn = v.pop().unwrap(); v.push(conn); let conn = v.pop().unwrap(); v.push(conn); v.clear(); println!("{}", v.len()) }

This friction is a genuine adjustment for developers coming from C++, where the compiler only checks that code is well-formed enough to produce machine code — not that it is memory-safe. Rust rejects an infinite number of memory-safe, useful programs it cannot verify. The next-generation borrow checker, Polonius, is under active development within the Rust project.

Escape Hatches Without unsafe

When the borrow checker blocks a design, there are alternatives that do not require dropping into unsafe code. One common pattern defers borrow-checking to runtime using reference-counted cells:

use std::cell::RefCell; use std::rc::Rc; struct Conn {} impl Conn { fn mutate(&mut self) { // TODO: mutate self } } fn main() { let conn = Rc::new(RefCell::new(Conn {})); let mut v = vec![]; v.push(Rc::clone(&conn)); { // we can borrow mutably here if we want v[0].borrow_mut().mutate(); } v.clear(); println!("{}", v.len()) }

This works well for single-threaded code: runtime checks enforce AXM at a small cost each time borrow_mut() or borrow() is called. Arenas are another approach; a search on lib.rs offers starting points. Properly verified unsafe code can also back useful data structures that are then exposed through safe APIs — the standard library is not the limit. The im crate, for instance, provides immutable data structures, and several "small string" crates store character data inline when possible.

From Single-Threaded to Concurrent

In multi-threaded programs, Rc<RefCell<...>> commonly becomes Arc<Mutex<...>>. Arc is the atomic counterpart of Rc, using more expensive synchronization primitives but being safe to share across threads. Mutex differs from RefCell by waiting for a value to become available rather than returning an error on conflict — which can lead to deadlocks. These are annoying but not memory-unsafe. Tooling exists for the problem too: parking_lot ships an experimental deadlock detector.

Amos

This complexity can intimidate at first, especially compared with garbage-collected languages like JavaScript or Go, or with C, which offers many pointer types but still demands extreme care. The extra machinery in Rust simply encodes the realities of multi-threaded data handling into something the compiler verifies before the program ever runs. C and C++ gamble on allocators, fuzzers, or sanitizers catching problems; JavaScript and Go only discover errors at runtime, paying for checks on every operation and making lasting confidence hard to earn.

Amos

Designing for the Compiler

The borrow checker shapes program structure, encouraging organization by "when data is mutated" rather than by theme. For experienced C or C++ developers, accepting this constraint takes time — often several attempts — but when it clicks, the payoff is not just memory safety but more correct programs. A type system that works with you enables designs that would be reckless in C or C++.

Amos

That is the promise of Rust.