Chapter 3: Borrowing
Remember in the last chapter when we passed a value to a function and lost ownership of it? That was annoying, right? You had to either clone the value (wasteful) or restructure your code to keep ownership in main.
There's a better way: borrowing.
What Is Borrowing?
Borrowing is creating a reference to a value without taking ownership. The reference lets you use the value, but the original owner remains in charge of dropping it.
Think of it like borrowing a book from a library. You don't own the book — you just get to read it. The library (owner) still decides what happens to it. When you're done, you return the reference. The book itself never left the building.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // & means "borrow s1"
println!("The length of '{}' is {}", s1, len); // s1 is still valid!
}
fn calculate_length(s: &String) -> usize {
s.len()
} // s goes out of scope, but it didn't own the String — nothing is dropped
The &s1 passes a reference to s1. Inside calculate_length, the parameter s is a reference to a String. When the function ends, s goes out of scope — but it was never an owner, so nothing gets dropped. s1 in main is still perfectly valid.
This is the core of borrowing: you get to use data without owning it.
The Two Rules of Borrowing
Rust's borrow checker enforces two rules at compile time:
- You can have either one mutable reference OR any number of immutable references.
- A reference must always be valid.
These rules eliminate data races at compile time. Let's break them down.
Immutable References
An immutable reference lets you read data but not modify it:
fn main() {
let s = String::from("hello");
let s1 = &s; // immutable borrow
let s2 = &s; // another immutable borrow — totally fine!
println!("{} and {}", s1, s2); // both can read
// s is still valid here — we only borrowed immutably
println!("{}", s);
}
Multiple immutable borrows are allowed because reading shared data isn't dangerous. It's when we start mutating that Rust gets strict.
Mutable References
If you want to modify the data, you need a mutable reference:
fn main() {
let mut s = String::from("hello");
change(&mut s); // mutable borrow
println!("{}", s); // prints "hello, world"
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
The &mut syntax creates a mutable reference. And here's where Rust gets interesting:
fn main() {
let mut s = String::from("hello");
let s1 = &mut s; // mutable borrow
let s2 = &mut s; // ERROR: cannot borrow s as mutable more than once at a time
println!("{} {}", s1, s2);
}
This won't compile. You can't have two mutable references to the same data at the same time. Why? Because that would create a data race — two places in your code trying to modify the same memory simultaneously.
The Rationale
This seems restrictive. It is restrictive. But consider what happens in other languages without these rules:
// JavaScript — completely legal, potentially disastrous
let data = [1, 2, 3];
const reader = data;
const writer = data;
reader.push(99); // modifies the array
writer.push(88); // modifies the same array
console.log(data); // what do you get? depends on order!
In JavaScript, both reader and writer reference the same array. The behavior depends on execution order — a classic race condition. These bugs are notoriously hard to find because they might only manifest under specific timing.
Rust makes this impossible to write. At compile time. No runtime checks needed.
Mixing Immutable and Mutable
You also can't hold an immutable reference while creating a mutable one:
fn main() {
let mut s = String::from("hello");
let s1 = &s; // immutable borrow
let s2 = &mut s; // ERROR: cannot borrow as mutable while immutable borrow exists
println!("{} and {}", s1, s2);
}
The immutable borrow (s1) promises "I won't change this." Creating a mutable borrow (s2) would break that promise. Rust prevents this at compile time.
Dangling References
The second rule — "a reference must always be valid" — prevents dangling pointers:
fn main() {
let reference = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s // ERROR: returns reference to local variable
} // s goes out of scope here, memory is freed — reference is now invalid!
In C/C++, returning a pointer to a local variable is a classic bug. The memory gets freed when the function returns, but the pointer still exists, pointing to garbage. This is a "use-after-free" — one of the most dangerous bugs in computing.
Rust won't let you write this. The compiler sees that s goes out of scope at the end of dangle, so returning &s is impossible. It won't compile.
The Pattern in Practice
Here's code that actually compiles and demonstrates these rules:
fn main() {
let mut s = String::from("hello");
// Immutable borrows first
let s1 = &s;
let s2 = &s;
println!("{} and {}", s1, s2);
// After s1 and s2 go out of scope, we can borrow mutably
let s3 = &mut s;
s3.push_str(", world");
println!("{}", s3);
}
The immutable references s1 and s2 go out of scope before we create s3. This is perfectly legal — Rust tracks when each reference is last used and allows mutable borrowing after that point.
Why This Matters
Borrowing lets you:
- Use large data without cloning — pass references to functions instead of moving ownership
- Share data safely — immutable references can be shared freely
- Modify data efficiently — mutable references give you in-place updates
- Never worry about dangling pointers — the compiler guarantees validity
The borrow checker seems frustrating at first. You'll fight it. You'll write code that feels "obviously correct" and watch Rust reject it with cryptic errors.
But here's the thing: every error the borrow checker catches is a bug you won't have to debug at 3am. Every restriction prevents a race condition you wouldn't have found until production. The compiler is working for you, even when it feels like it's working against you.
Next: Chapter 4 — Structs & Methods