References and borrowing
Let functions read and modify values without taking ownership — using Rust's shared and exclusive reference rules.
- Explain what a reference is and how it differs from ownership
- Create shared references with &T and exclusive references with &mut T
- State the two borrowing rules and explain why they exist
- Write function signatures that borrow rather than take ownership
- Recognise the compiler error for simultaneous mutable and immutable borrows
The beginner track showed that passing a String to a function moves ownership into that function — which means you can't use the original variable afterwards. That's often not what you want. References solve this: they let you hand a function a pointer to your data without transferring ownership. Looking at data without owning it is called borrowing.
This is the idiomatic solution to most ownership friction in Rust. Understanding references well means you'll spend far less time arguing with the borrow checker.
Shared references: &T
A shared reference (&T) gives read-only access to a value. The original owner keeps ownership; the reference is just a pointer with a guaranteed-valid lifetime.
fn length(s: &String) -> usize {
s.len()
}
fn main() {
let name = String::from("Ferris");
let n = length(&name); // borrow, not move
println!("{} has {} chars", name, n); // name is still valid
}The & in the call site (&name) creates a reference. The &String in the signature says "I expect a reference, not an owned String." After length returns, the borrow ends and name remains the owner.
You can have any number of shared references to a value at the same time. Multiple readers are safe because none of them can mutate the data.
Exclusive references: &mut T
A mutable reference (&mut T) allows mutation. The key constraint: you can have at most one mutable reference at a time, and no shared references may coexist with it.
fn shout(s: &mut String) {
s.push_str("!!!");
}
fn main() {
let mut greeting = String::from("Hello");
shout(&mut greeting);
println!("{}", greeting); // Hello!!!
}Note: the variable (greeting) must be declared mut before you can create a &mut reference to it.
The two borrowing rules
Rust's borrow checker enforces two rules at compile time:
- At any given time, you can have either one
&mut Tor any number of&T— but not both. - References must always be valid — they cannot outlive the value they point to.
These rules prevent two classes of bugs that are common in C and C++: data races (two threads mutating the same memory concurrently) and dangling pointers (a reference to memory that has been freed).
let mut s = String::from("hello");
let r1 = &s; // fine
let r2 = &s; // fine — multiple shared refs allowed
let r3 = &mut s; // ERROR: s is already borrowed immutably
println!("{}, {}, {}", r1, r2, r3);error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutableThe compiler is precise about where borrows start and end (using a feature called Non-Lexical Lifetimes). If r1 and r2 are no longer used before r3 is created, the code compiles fine:
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} and {}", r1, r2); // r1 and r2 last used here
let r3 = &mut s; // fine now — r1 and r2's borrows have ended
println!("{}", r3);Dangling references are impossible
The compiler ensures you can never create a reference that outlives its data:
fn dangle() -> &String { // ERROR: returns a reference to dropped data
let s = String::from("hello");
&s // s is dropped when this function returns — the ref would dangle
}The fix is to return the owned String directly, not a reference to it. The compiler explains this in the error and suggests exactly that.
References in function signatures
References are idiomatic for parameters that don't need to own data. A few patterns you'll see everywhere:
// read-only access — use a shared ref
fn word_count(text: &str) -> usize {
text.split_whitespace().count()
}
// mutation without ownership transfer
fn normalise(v: &mut Vec<i32>) {
v.sort();
v.dedup();
}
// taking ownership — only when the function needs to store or drop the value
fn consume(s: String) {
println!("Using: {}", s);
} // s dropped hereChoose &T by default. Reach for &mut T only when mutation is needed. Take ownership (T) only when the function genuinely needs to own the value.
A common early Rust pattern is to clone() whenever the borrow checker complains. This works, but costs a heap allocation. Most of the time, switching to a reference is the right answer — and it makes the intent clearer too.
Check your understanding
Knowledge check
- 1.How many shared (&T) references to the same value can you have at once?
- 2.Why does Rust forbid having a &mut T alongside a &T at the same time?
- 3.In Rust, a function can safely return a reference to a local variable it created.
Do it yourself
Try writing a function that takes &mut Vec<i32> and appends the sum of all elements to the vec. Observe how the borrow checker prevents you from keeping a reference into the vec while you also hold a mutable reference to compute the sum:
fn append_sum(v: &mut Vec<i32>) {
let total: i32 = v.iter().sum();
v.push(total);
}The compiler's error messages will explain exactly what conflicts. Learning to read them carefully is the fastest path to writing idiomatic Rust.
Where to go next
Now that you can borrow single values, the next lesson looks at slices — borrows that cover a range of elements in a string or array, rather than a whole value.