Mutable References in Rust
Immutable references let you read data without taking ownership. But what if you need to modify borrowed data? That's where mutable references come in. They give temporary write access to a value — but with strict rules that prevent data races at compile time.
Creating a Mutable Reference: &mut T
A mutable reference is created with &mut and its type is written as &mut T. To borrow a value mutably, the original variable must itself be declared mut.
fn main() {
let mut s = String::from("hello"); // must be mut to borrow mutably
change(&mut s); // pass a mutable reference
println!("{}", s); // "hello, world"
}
fn change(s: &mut String) { // accepts a mutable reference
s.push_str(", world"); // modifying through the reference is fine
}The Golden Rule of Mutable References
Rust enforces one fundamental rule about mutable references that prevents entire classes of bugs:
This rule exists to prevent data races — a class of bug where two pointers access the same memory simultaneously, at least one writes, and there's no synchronization. Data races cause undefined behaviour in C and C++ and are notoriously hard to debug. Rust eliminates them entirely at compile time.
Compiler Error: Two Mutable References
Trying to create two mutable references to the same value simultaneously is a compile error:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s; // first mutable reference
let r2 = &mut s; // ERROR: cannot borrow `s` as mutable more than once
println!("{}, {}", r1, r2);
}error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used hereThe compiler tells you exactly what went wrong, where each borrow occurred, and where the first borrow is still in use. These error messages are designed to be read and acted on.
Compiler Error: Mixing &T and &mut T
You also cannot have an immutable reference and a mutable reference to the same value at the same time:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // immutable reference
let r2 = &s; // second immutable reference — this is fine
let r3 = &mut s; // ERROR: cannot borrow as mutable while immutable refs exist
println!("{}, {}, {}", r1, r2, r3);
}error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s;
| -- immutable borrow occurs here
5 | let r2 = &s;
6 | let r3 = &mut s;
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}, {}, {}", r1, r2, r3);
| -- immutable borrow later used hereThe reasoning: if a mutable reference existed alongside immutable ones, the immutable readers could observe data changing under them — violating the guarantee that their view is stable. Rust catches this statically.
Non-Lexical Lifetimes (NLL)
Since Rust 2018, the borrow checker uses Non-Lexical Lifetimes (NLL). Instead of treating a borrow as lasting until the end of the enclosing scope (the opening { to closing }), the compiler tracks when the last use of each reference occurs.
This means references end at their last use, not at the closing brace:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this line — their borrows end here
let r3 = &mut s; // OK! immutable refs r1 and r2 are already done
println!("{}", r3);
}The Variable Must Be Declared mut
You cannot take a mutable reference to an immutable variable. The mut keyword on the variable declaration is what grants mutable borrow permission:
fn main() {
let s = String::from("hello"); // NOT declared mut
// let r = &mut s; // ERROR: cannot borrow `s` as mutable, as it is not declared as mutable
let mut t = String::from("hello"); // declared mut
let r = &mut t; // OK
r.push_str(", world");
println!("{}", r);
}error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
--> src/main.rs:4:13
|
2 | let s = String::from("hello");
| - help: consider changing this to be mutable: `mut s`
3 |
4 | let r = &mut s;
| ^^^^^^ cannot borrow as mutableMutable References in Function Parameters
When a function needs to modify the data it receives, it takes a mutable reference parameter. This makes the intent explicit in the function signature — callers know the function will mutate their data.
fn append_exclamation(s: &mut String) {
s.push('!');
}
fn double_vec(v: &mut Vec<i32>) {
let len = v.len();
for i in 0..len {
let val = v[i];
v.push(val);
}
}
fn reset_to_zero(n: &mut i32) {
*n = 0; // must deref explicitly for primitive types
}
fn main() {
let mut greeting = String::from("Hello");
append_exclamation(&mut greeting);
println!("{}", greeting); // Hello!
let mut numbers = vec![1, 2, 3];
double_vec(&mut numbers);
println!("{:?}", numbers); // [1, 2, 3, 1, 2, 3]
let mut count = 42;
reset_to_zero(&mut count);
println!("{}", count); // 0
}Practical Pattern: Modifying Vector Elements
A common use of mutable references is modifying individual elements in a collection in place:
fn main() {
let mut scores = vec![70, 85, 92, 60, 78];
// Boost all failing scores
for score in &mut scores {
if *score < 75 {
*score += 10; // dereference to modify
}
}
println!("{:?}", scores); // [80, 85, 92, 70, 88]
}
// Also works with a function
fn normalize(values: &mut Vec<f64>, factor: f64) {
for v in values.iter_mut() {
*v /= factor;
}
}
fn main_normalize() {
let mut data = vec![100.0, 200.0, 300.0];
normalize(&mut data, 100.0);
println!("{:?}", data); // [1.0, 2.0, 3.0]
}Practical Pattern: Updating Struct Fields
struct Player {
name: String,
score: u32,
level: u32,
}
fn award_points(player: &mut Player, points: u32) {
player.score += points;
if player.score >= player.level * 100 {
player.level += 1;
println!("{} leveled up to level {}!", player.name, player.level);
}
}
fn main() {
let mut p = Player {
name: String::from("Alice"),
score: 0,
level: 1,
};
award_points(&mut p, 60);
award_points(&mut p, 50); // triggers level up — score hits 110
println!("Score: {}, Level: {}", p.score, p.level);
}Alice leveled up to level 2! Score: 110, Level: 2
Using Scopes to Enable Multiple Mutations
If you genuinely need multiple mutable references, you can use explicit scopes to ensure they don't overlap. The borrow ends when the reference goes out of scope:
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
r1.push_str(", world");
} // r1 goes out of scope here — mutable borrow ends
let r2 = &mut s; // OK now — r1 is gone
r2.push('!');
println!("{}", s); // hello, world!
}Mutable References vs Mutable Variables
Concept | Syntax | What It Means |
|---|---|---|
Mutable variable |
| x itself can be reassigned or modified |
Mutable reference |
| r allows modifying the data x owns |
Immutable ref to mut var |
| Can read x but not modify through r |
Mut ref requires mut var |
| Cannot mutably borrow an immutable binding |
Interior Mutability: A Preview of RefCell
Sometimes you need multiple handles to the same data, with the ability to mutate it — but Rust's static rules prevent this. For these cases, Rust offers interior mutability patterns, where mutation is checked at runtime instead of compile time.
The most common tool for this is RefCell<T>:
use std::cell::RefCell;
fn main() {
// RefCell allows mutation through shared (&) references
let data = RefCell::new(vec![1, 2, 3]);
// Borrow immutably
println!("{:?}", data.borrow()); // [1, 2, 3]
// Borrow mutably — checked at runtime, not compile time
data.borrow_mut().push(4);
println!("{:?}", data.borrow()); // [1, 2, 3, 4]
}Why the Rules Make Rust Safer
The mutable reference rules eliminate two major categories of bugs:
Bug Type | What Happens in C/C++ | What Rust Does |
|---|---|---|
Data race | Two threads write to same memory — undefined behaviour | Compile error: only one &mut T at a time |
Iterator invalidation | Modifying a vector while iterating it crashes | Compile error: cannot have &mut and & simultaneously |
Unexpected mutation | Function mutates data caller didn't expect to change | Caller must explicitly pass &mut — mutation is visible at call site |
Aliased mutation | Two pointers modify the same memory unpredictably | Impossible: &mut T is always exclusive |
Summary
Mutable references (
&mut T) allow modifying borrowed dataThe original variable must be declared
mutto take a mutable referenceAt any given time: any number of
&TOR exactly one&mut T— never bothThis rule prevents data races at compile time, with zero runtime cost
Two simultaneous
&mutreferences to the same value are a compile errorAn
&Tand&mut Tto the same value cannot coexistNon-Lexical Lifetimes: borrows end at last use, not end of scope
Use explicit scopes to sequence mutations when NLL isn't enough
RefCell provides runtime-checked interior mutability for special cases