Rust Borrow Checker: Immutable and Mutable References Cannot Alias
Rust's borrow checker prevents data races without garbage collection by enforcing that mutable and immutable references cannot alias the same data simultaneously.
16 Oct 2025, 15:48 UTC

The problem: memory safety without a garbage collector
Rust does not use a garbage collector to reclaim memory. Instead, the borrow checker enforces memory safety at compile time by tracking ownership and borrowing. The central rule is that at any moment, a value can have either any number of immutable references or exactly one mutable reference, but never both at the same time.
An immutable reference, written &T, lets you read data. A mutable reference, written &mut T, lets you read and write data. If an immutable borrow is still active when you try to create a mutable borrow, the compiler rejects the program. This prevents data races and dangling pointers before the code runs.
Useful takeaway
You can have many readers or one writer, but not both at once. If you structure your code so that read-only borrows end before a mutable borrow begins, the borrow checker accepts the code.
Worked example: shared reads, then exclusive write
String is an owned, heap-allocated text type. The example below creates two immutable borrows, uses them, then creates one mutable borrow after the immutable borrows are no longer needed.
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{}{}", r1, r2);
let r3 = &mut s;
println!("{}", r3);
}
This compiles under Rust 1.70 or later with edition 2018 or newer. The compiler uses non-lexical lifetimes: it recognizes that r1 and r2 are last used in the first println!, so their borrows end before r3 takes exclusive access to s.
What breaks the rule
Using an immutable borrow after a mutable borrow is created causes a compile error. The following code fails because r1 and r2 would still be active when r3 requests mutable access:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
println!("{}{}", r1, r2);
}
The compiler reports that s cannot be borrowed as mutable because it is also borrowed as immutable. No runtime check is involved; the error appears during compilation.
Running and verifying the example
- Check the toolchain:
rustc --version. Expect version 1.70 or later. - Save the first example to
borrow_example.rs. - Compile it:
rustc borrow_example.rs. This runs with normal user permissions and produces a binary in the current directory. - Run the binary:
./borrow_exampleon Linux or macOS, orborrow_example.exeon Windows. - Edit the file so
r1orr2is used afterr3is created, then compile again. The compiler should emit an error and refuse to produce a binary.
Risk: compiling a single source file with rustc and no dependencies does not execute build scripts or procedural macros, so the supply-chain risk is minimal for this example.
Limits and common mistakes
Returning a reference to a local variable
A frequent mistake is trying to return a reference to data created inside a function:
fn dangling() -> &String {
let s = String::from("temporary");
&s
}
This fails because s is dropped when the function ends. A reference to it would dangle, so the borrow checker rejects the code. Return an owned String instead, or restructure the caller to own the data.
Self-referential structs
A struct that contains a reference to one of its own fields cannot be expressed safely in safe Rust because moving the struct would invalidate the reference. Such patterns require unsafe code or external crates such as ouroboros.
Interior mutability moves checks to runtime
Types such as RefCell<T> allow mutation through an immutable reference by checking borrow rules at runtime instead of compile time. Violating the rules causes a panic rather than a compile error. Use this only when the borrow checker cannot statically verify your pattern.
The borrow checker is conservative
Some valid programs are rejected because the compiler cannot prove the borrows do not overlap. In those cases, you may need to clone data, restructure lifetimes, or use interior mutability.
Practical check
Compile both examples with rustc. The first should produce a binary; the second should produce a borrow error. If you use Cargo, cargo check performs the same analysis without building a binary. This confirms that the compiler is enforcing the aliasing rule before execution.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.