Borrowing and References
How & and &mut let you use a value without taking ownership of it, and the rules the borrow checker enforces to keep it safe.
3 min read
The previous lesson showed the problem: passing a String into a function moves it, and the caller can't use it afterward. Rewriting every function to hand ownership back would be unbearable. Borrowing solves this — you pass a reference to a value instead of the value itself.
References with &
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}."); // s1 still valid!
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope here, but it doesn't own the data, so nothing is dropped&s1 creates a reference to s1 without transferring ownership — this is called borrowing. calculate_length can read the string through its reference, but when the function ends, only the reference goes away, not the underlying String. s1 is still perfectly valid back in main.
Mutable references
References are immutable by default, just like variables. To modify a borrowed value, use &mut:
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{s}"); // "hello, world"
}
fn change(s: &mut String) {
s.push_str(", world");
}The rule that prevents data races: one mutable reference at a time
This is where the borrow checker earns its reputation. For any given value, you can have either any number of immutable references or exactly one mutable reference — never both at once.
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // fine: multiple immutable borrows
let r3 = &mut s; // error: cannot borrow `s` as mutable
// because it is also borrowed as immutable
println!("{r3}");
}The compiler rejects this even though, at a glance, it looks harmless. The reason is that r1 and r2 might still be in use when r3 tries to mutate s — and a reader holding a reference while the underlying data changes underneath it is exactly the class of bug (a data race, in concurrent code) this rule exists to eliminate. Fix it by ensuring the immutable borrows are done being used before the mutable one starts:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{r1} and {r2}"); // r1, r2 no longer used after this point
let r3 = &mut s; // fine now
println!("{r3}");
}Dangling references are rejected too
C and C++ let you return a pointer to memory that's already been freed — a dangling pointer, and a classic source of crashes and security bugs. Rust makes this a compile error:
fn dangle() -> &String {
let s = String::from("hello");
&s
} // error: `s` is dropped here, but a reference to it is being returneds is local to dangle and dropped when the function ends — the compiler refuses to let a reference to it escape. The fix is to return the owned String itself, transferring ownership out, rather than a reference to something about to disappear:
fn no_dangle() -> String {
let s = String::from("hello");
s // ownership moves out to the caller
}Why this is worth the friction
Every one of these rules is enforced entirely at compile time, with zero runtime cost — no reference counting, no locks, nothing checked while your program actually runs. The tradeoff is that you sometimes need to restructure code to satisfy the checker. In exchange, entire categories of bugs that are notoriously hard to debug in C or C++ — use-after-free, dangling pointers, data races — simply cannot happen in safe Rust. They're caught here, at compile time, instead of in production.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.