Mutable References and the Borrow Checker
A reference lets you access a value without taking ownership of it — you borrow it instead of moving it. Rust references come in two flavors: immutable (&T) and mutable (&mut T), and the compiler’s borrow checker enforces strict rules about when each kind may exist. Mutable references let you modify data through a function call without constantly moving ownership back and forth, but the checker will reject any code where that access could cause a data race or a dangling pointer — even in a single-threaded program. This lesson explains exactly how &mut works, why the checker’s rules exist, and how to read and fix the compile errors you will inevitably run into.
Overview / How it works
Think of a value in Rust like a shared document. If several people only want to read the document, that’s fine — any number of readers can look at it at once without stepping on each other. But if someone wants to edit it, every other viewer needs to close their copy first, otherwise one person could be reading a sentence the other is halfway through rewriting. Rust’s borrow checker enforces exactly this rule for every value, at compile time, with zero runtime cost: at any given point in the program, you may have either any number of immutable references (&T) to a value, or exactly one mutable reference (&mut T) to it — never both at the same time. This is sometimes called “aliasing XOR mutability”: a value can be aliased (many read-only views) or mutable (one exclusive view), but never both simultaneously.
Why does this matter? Without this rule, you could have one reference reading a Vec while another reference resizes it, invalidating the first reference’s underlying memory — a classic use-after-free bug that plagues C and C++ programs. In multi-threaded code, the same pattern is a data race. Rust closes off both problems at compile time by refusing to let a mutable and an immutable borrow of the same value coexist, so there’s no way to accidentally observe or corrupt memory mid-mutation. No garbage collector and no runtime lock are needed — the checker proves the property once, before the program even runs.
To create a mutable reference, the variable being borrowed must itself be declared mut, and you write &mut in front of it. Inside the function that receives it, you use the dereference operator * to read or write through the reference. Consider this trace:
let mut x = 5; creates an owned, mutable integer.let r = &mut x; creates a mutable reference to x. From this point until r is last used, x itself cannot be read or written directly, and no other reference to x may exist.*r += 1; dereferences r to reach the integer it points to and increments it.
After the last use of r, the borrow ends and x becomes freely accessible again.
That last point is important: modern Rust uses non-lexical lifetimes (NLL), meaning a borrow’s lifetime ends at its last use, not at the end of its enclosing block. This is why some code that looks like it should conflict actually compiles fine — the checker is smarter than simple scope-based reasoning.
Syntax
&T // an immutable (shared) reference to a value of type T
&mut T // a mutable (exclusive) reference to a value of type T
&mut x // create a mutable reference to variable x (x must be declared `mut`)
*r // dereference r: read or write the value it points to
fn f(v: &mut Vec<i32>) { ... } // function taking a mutable reference as a parameter
| Piece | Meaning |
|---|---|
let mut x = ...; |
Declares x as a mutable binding — required before you can create &mut x. |
&mut x |
Borrows x exclusively; no other reference (mutable or immutable) to x may be alive at the same time. |
*r |
Dereferences a reference r to access or assign the underlying value. |
fn f(p: &mut T) |
A function parameter that borrows its argument mutably instead of taking ownership. |
Examples
Example 1: A function that mutates through a reference.
fn main() {
let mut count = 5;
add_one(&mut count);
println!("count is now {}", count);
}
fn add_one(n: &mut i32) {
*n += 1;
}
Output:
count is now 6
add_one never takes ownership of count; it borrows it exclusively for the duration of the call, dereferences it with *n to add one, and the change is visible in main once the function returns — because it modified the original count, not a copy.
Example 2: Many immutable borrows, then one mutable borrow.
fn main() {
let mut s = String::from("hello");
{
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
}
let r3 = &mut s;
r3.push_str(", world");
println!("{}", r3);
}
Output:
hello and hello
hello, world
r1 and r2 are both immutable, so they’re allowed to coexist. The inner block ends their lifetime (their last use is the println! right before the closing brace), so by the time r3 is created, no other reference to s is alive — the exclusive mutable borrow is legal.
Example 3: Mutating a collection in place through a function.
fn main() {
let mut scores = vec![10, 20, 30];
double_scores(&mut scores);
println!("{:?}", scores);
}
fn double_scores(scores: &mut Vec<i32>) {
for score in scores.iter_mut() {
*score *= 2;
}
}
Output:
[20, 40, 60]
double_scores takes &mut Vec<i32> instead of the vector by value, so main keeps ownership of scores and can still use it after the call. Inside, iter_mut() hands out a mutable reference to each element, and *score *= 2 writes through it. This is the idiomatic way to let a function modify a caller’s data without moving or cloning it.
How it works step by step
Walking through Example 2, here is what the borrow checker actually verifies:
1. let r1 = &s; — an immutable borrow of s begins.
2. let r2 = &s; — a second immutable borrow begins; this is fine because immutable borrows can stack freely.
3. println!("{} and {}", r1, r2); — both borrows are used here; this is their last use, so both borrows end immediately after this line (thanks to non-lexical lifetimes).
4. let r3 = &mut s; — the checker confirms no other reference to s is currently alive, so this exclusive borrow is granted.
5. r3.push_str(", world"); — mutation happens through r3, safe because it’s the only reference in existence.
6. println!("{}", r3); — last use of r3; the mutable borrow ends here.
If step 3 were moved to after step 4 — that is, if you tried to print r1 after creating r3 — the checker would see r1‘s lifetime overlapping with r3‘s and reject the program, because that would mean one immutable and one mutable reference to s alive at the same instant.
Common Mistakes
Mistake 1: Using an immutable reference after a mutable one has started.
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s;
println!("{} and {}", r1, r2);
}
This fails to compile with an error like cannot borrow `s` as mutable because it is also borrowed as immutable. The problem is that r1 is used in the final println!, which is after r2 is created — so the checker sees r1‘s borrow overlapping with r2‘s exclusive borrow. Fix it by finishing with r1 before creating the mutable reference:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1);
let r2 = &mut s;
r2.push_str(", world");
println!("{}", r2);
}
Output:
hello
hello, world
Mistake 2: Two mutable references to the same value at once.
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{} and {}", r1, r2);
}
This fails with cannot borrow `s` as mutable more than once at a time. Both r1 and r2 are alive at the final println!, which would let two different references write to s simultaneously — exactly what the exclusivity rule forbids, even though this program is single-threaded. Fix it by letting each mutable borrow finish before starting the next:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
r1.push_str(" world");
println!("{}", r1);
let r2 = &mut s;
r2.push_str("!");
println!("{}", r2);
}
Output:
hello world
hello world!
Two other pitfalls worth watching for: forgetting mut on the original binding (let s = String::from("hi"); &mut s; fails because s was never declared mutable), and trying to return a mutable reference to a value that was created inside the function and goes out of scope at the end — the checker rejects this as a dangling reference, and the fix is almost always to return an owned value instead.
Best Practices
- Prefer borrowing (
&Tor&mut T) over passing by value when a function doesn’t need to take ownership — it avoids unnecessary moves and clones. - Keep mutable borrows as short-lived as possible; the sooner a
&mutreference’s last use happens, the sooner the value becomes available again for other borrows. - Reach for
&mutonly when a function genuinely needs to modify the caller’s data in place; if it just reads, use&Tso callers can still use other immutable references concurrently. - When iterating and mutating a collection’s elements, use
.iter_mut()rather than indexing in a loop — it expresses intent clearly and lets the checker verify safety easily. - If you find yourself fighting the borrow checker over shared mutable state, consider whether the design actually needs shared ownership (
Rc<RefCell<T>>or, across threads,Arc<Mutex<T>>) rather than restructuring code awkwardly to satisfy simple borrowing. - Read borrow-checker error messages carefully — they usually name the exact lines where each conflicting borrow starts and is last used, which is the fastest way to find the fix.
- Don’t reach for
unsafeto bypass a borrow-checker error you don’t understand yet; almost every legitimate program can be restructured to satisfy the checker.
Practice Exercises
Exercise 1: Write a function fn append_exclaim(s: &mut String) that appends "!" to the given string. Call it from main on a String containing "hi" and print the result. Expected output: hi!
Exercise 2: Write a function fn clear_negatives(v: &mut Vec<i32>) that removes all negative numbers from the vector in place (hint: look at Vec::retain, which takes a closure and keeps only the elements for which it returns true). Test it on vec![3, -1, 4, -5, 9]; expected output: [3, 4, 9].
Exercise 3: Try writing a small program where you create an immutable reference to a String, then immediately try to create a mutable reference to the same String while the immutable one is still in use in a later println!. Read the compiler error it produces, then fix the code by reordering the borrows so their lifetimes don’t overlap.
Summary
- A reference borrows access to a value without taking ownership;
&Tis immutable and&mut Tis mutable. - At any point, a value may have either any number of immutable references or exactly one mutable reference — never both at once.
- This rule is enforced entirely at compile time by the borrow checker, with no runtime cost, and it prevents data races and use-after-free bugs by construction.
- Non-lexical lifetimes mean a borrow ends at its last use, not at the end of its enclosing block, which is why reordering code can turn a compile error into valid code.
- Use
*rto read or write through a reference, and remember the original variable must be declaredmutbefore you can create a&mutreference to it. - Prefer borrowing over taking ownership when a function only needs temporary access, and keep mutable borrows as short as possible.
- When the checker rejects your code, its error message almost always points directly at the conflicting borrows — read it before reaching for a workaround.
