What is Borrowing?
Instead of transferring ownership, you can borrow a value by creating a reference. References allow you to use a value without taking ownership, so the original owner remains valid. This is Rust's answer to the "I want to read data without consuming it" problem.
fn main() {
let s = String::from("hello");
// &s creates a reference (borrow)
let len = calculate_length(&s);
// s is still valid because we only borrowed it!
println!("'{s}' has length {len}");
}
// &String means "a reference to a String"
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but since it doesn't own
// the data, nothing is dropped
Borrowing Rules
At any given time, you can have either:
- Any number of immutable references (
&T) — multiple readers are safe - OR exactly one mutable reference (
&mut T) — one writer with exclusive access
You cannot have a mutable reference while immutable references exist. This prevents data races at compile time.
Immutable References (&T)
fn main() {
let s = String::from("hello");
// Multiple immutable references are fine
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{r1}, {r2}, {r3}"); // All valid
// Cannot modify through an immutable reference
// r1.push_str(" world"); // ERROR: cannot borrow as mutable
}
fn print_greeting(name: &str) {
println!("Hello, {name}!");
// name is read-only here
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}
Mutable References (&mut T)
fn main() {
let mut s = String::from("hello");
// One mutable reference at a time
change(&mut s);
println!("{s}"); // "hello, world"
// This won't compile — two mutable refs at once:
// let r1 = &mut s;
// let r2 = &mut s;
// println!("{r1}, {r2}");
// But sequential mutable borrows are fine:
let r1 = &mut s;
r1.push_str("!");
// r1 is no longer used after this point
let r2 = &mut s; // OK — r1's borrow has ended
r2.push_str("!");
println!("{s}");
}
fn change(s: &mut String) {
s.push_str(", world");
}
Cannot Mix Mutable and Immutable
fn main() {
let mut s = String::from("hello");
let r1 = &s; // Immutable borrow
let r2 = &s; // Another immutable borrow — fine
// let r3 = &mut s; // ERROR: cannot borrow as mutable
// // because it's also borrowed as immutable
println!("{r1} and {r2}");
// r1 and r2 are no longer used after this point (NLL)
// Now a mutable borrow is OK!
let r3 = &mut s;
r3.push_str(" world");
println!("{r3}");
}
Non-Lexical Lifetimes (NLL)
Since Rust 2018, the compiler uses Non-Lexical Lifetimes. A reference's lifetime ends at its last use, not at the end of the enclosing scope. This makes the borrow checker much more ergonomic.
fn main() {
let mut data = vec![1, 2, 3];
// Before NLL, this wouldn't compile because r's scope
// extended to the end of the block
let r = &data;
println!("Immutable: {:?}", r);
// NLL: r's borrow ends here (last use)
// Now we can mutably borrow
data.push(4);
println!("Mutated: {:?}", data);
}
Dangling Reference Prevention
// Rust prevents dangling references at compile time!
// This won't compile:
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // ERROR: s is dropped at end of function,
// // so the reference would be dangling!
// }
// Fix: return the owned value instead
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership is moved out — no dangling reference
}
fn main() {
let s = no_dangle();
println!("{s}");
}
Key Takeaways
- ✅ References (&T) borrow data without taking ownership
- ✅ You can have many immutable references OR one mutable reference — never both
- ✅ Mutable references (&mut T) require the original variable to be declared
mut - ✅ NLL makes the borrow checker smarter — borrows end at last use, not scope end
- ✅ The compiler prevents dangling references entirely at compile time