Borrowing
Borrowing lets you use a value without taking ownership of it. You do this with references.
- Immutable borrow →
&T - Mutable borrow →
&mut T
Rust enforces strict rules so memory stays safe at compile time, with zero runtime cost.
Why Borrowing Exists
Without borrowing, you’d constantly move ownership into functions and back out:
fn print(s: String) -> String {
println!("{}", s);
s
}
That’s clunky.
Borrowing solves this by allowing temporary access without transferring ownership.
Immutable Borrowing (&T)
- Read-only access
- Any number of immutable borrows allowed
- Ownership stays with the original variable
fn print_length(s: &String) {
println!("Length: {}", s.len());
}
fn main() {
let s = String::from("hello");
print_length(&s);
print_length(&s);
println!("{}", s); // still valid
}
sowns theString&screates an immutable reference- The function borrows the value
- No ownership is transferred
- Multiple immutable borrows are allowed
Think of immutable borrows as many people reading the same book—no one can change it.
Mutable Borrowing (&mut T)
- Read and write access
- Exactly one mutable borrow at a time
- The original value must be declared
mut
fn add_world(s: &mut String) {
s.push_str(" world");
}
fn main() {
let mut s = String::from("hello");
add_world(&mut s);
println!("{}", s); // "hello world"
}
sowns theString&mut screates a mutable reference- The function can modify the data
- Ownership remains in
main
Think of mutable borrowing as one person editing the document—no one else can even read it at the same time.
The Golden Borrowing Rules
Rust enforces these rules at compile time:
Rule 1: Multiple immutable borrows OR one mutable borrow
✅ Allowed:
let r1 = &s;
let r2 = &s;
✅ Allowed:
let r = &mut s;
❌ Not allowed:
let r1 = &s;
let r2 = &mut s; // ERROR
❌ Not allowed:
let r1 = &mut s;
let r2 = &mut s; // ERROR
Why this rule exists
It prevents data races:
- One thread reads while another writes → inconsistent state
- Two writers at once → corrupted data
Rust stops these problems before your code runs.
Borrow Scopes (Non-Lexical Lifetimes)
Rust is smarter than it looks.
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // last use of r1
let r2 = &mut s; // ✅ allowed
r2.push_str(" world");
Even though r1 is still “in scope” textually, Rust sees it’s no longer used and ends the borrow early.
This is called Non-Lexical Lifetimes (NLL).
Mutable + Immutable Borrowing (Common Error)
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s; // ❌ error
Fix it like this:
let mut s = String::from("hello");
{
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
} // r1 and r2 go out of scope
let r3 = &mut s; // ✅ OK
Borrowing in Functions
Immutable borrow
fn calculate_length(s: &String) -> usize {
s.len()
}
Mutable borrow
fn append_exclamation(s: &mut String) {
s.push('!');
}
Using both safely
fn main() {
let mut s = String::from("Rust");
let len = calculate_length(&s);
append_exclamation(&mut s);
println!("{} ({})", s, len);
}
Borrowing Prevents Dangling References
Rust will not let references outlive the data they point to.
fn get_ref() -> &String {
let s = String::from("hello");
&s // ❌ compile-time error
}
Why?
sis dropped at the end of the function- The reference would point to freed memory
Rust refuses to compile this.
Borrowing with Slices (Very Common)
Slices are references too.
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &b) in bytes.iter().enumerate() {
if b == b' ' {
return &s[0..i];
}
}
&s[..]
}
&stris an immutable borrow of a string slice- No allocation
- No ownership transfer
Mental Model (This Helps a LOT)
- Ownership: who frees the memory?
- Immutable borrow (
&): read-only, many allowed - Mutable borrow (
&mut): read-write, only one allowed - Compiler = strict librarian
If Rust allows it, it’s safe. Period.
Why Borrowing Is a Big Deal
Borrowing gives Rust:
- Memory safety
- Thread safety
- Zero runtime overhead
- No data races
- No dangling pointers
And you get these guarantees without writing extra code.