Rust’s borrow checker enforces memory safety without a garbage collector. It does this by tracking:
Ownership — who owns a value
Borrowing — who is allowed to use that value temporarily
Lifetimes — how long references remain valid
The borrow checker’s job is simple: 👉 Make sure no one uses memory that has been freed or mutated unsafely.
Let’s break this down.
Every value in Rust has one owner.
let s = String::from("hello");
Here, s owns the string.
If you assign it to another variable:
let t = s;
Ownership moves to t. s is now invalid, you can’t use it anymore.
Why? Because Rust wants to prevent double frees and dangling pointers.
Borrowing lets you use a value without taking ownership.
There are two kinds:
&T)You can have any number of these.
let s = String::from("hello");
let a = &s;
let b = &s; // fine
&mut T)You can have only one at a time.
let mut s = String::from("hello");
let m = &mut s; // ok
let n = &mut s; // ❌ not allowed
Why? To prevent data races — even in single-threaded code.
Rust enforces:
| Borrow Type | Allowed Simultaneously |
|---|---|
| Many immutable borrows | ✔️ Yes |
| One mutable borrow | ✔️ Yes |
| Mutable + immutable at same time | ❌ No |
This rule alone explains 90% of borrow checker errors.
let mut s = String::from("hello");
let a = &s; // immutable borrow
let b = &mut s; // ❌ cannot borrow mutably while immutable borrow exists
The borrow checker stops you because:
a might still be using sb wants to mutate sLifetimes tell Rust how long a reference is valid.
You rarely need to write them yourself — Rust infers them — but the idea is:
A reference must never outlive the value it refers to.
Example:
let r;
{
let x = 5;
r = &x; // ❌ x will be dropped at end of block
}
println!("{}", r);
Rust prevents this because r would point to freed memory.
Here’s the mindset shift that makes Rust click:
Rust enforces:
You can have shared access OR exclusive access, but not both.
This is the same rule used in concurrent programming. Rust just applies it everywhere.
Suppose you write:
let mut s = String::from("hello");
let len = s.len(); // immutable borrow
s.push('!'); // ❌ mutable borrow while immutable borrow exists
Fix it by limiting the scope of the immutable borrow:
let mut s = String::from("hello");
{
let len = s.len(); // borrow ends here
}
s.push('!'); // now allowed
Or compute the length later.
It’s more like a strict but helpful mentor. When it complains, it’s usually because:
Once you internalize the rules, you start writing code the borrow checker likes, and your programs become safer and more predictable.