The Borrow Checker
The borrow checker tracks which bindings own data and which references borrow it. It rejects programs that could cause use-after-free, double-free, or data races.
Recipe
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let s1 = String::from("longer");
let len = {
let s2 = String::from("x");
longest(&s1, &s2)
};
println!("{len}");
}When to reach for this: Understanding compile errors about "borrowed value does not live long enough" or "cannot borrow as mutable".
Working Example
struct Cache { data: Vec<String> }
impl Cache {
fn get_or_insert(&mut self, key: &str) -> &String {
for s in &self.data {
if s == key { return s; }
}
self.data.push(key.to_string());
self.data.last().unwrap()
}
}
fn main() {
let mut c = Cache { data: vec![] };
let v = c.get_or_insert("a");
println!("{v}");
}What this demonstrates:
- Returning
&Stringfrom&mut selfextends borrow ofself - Caller holds shared loan while using return value
- Cannot mutate
cwhilevborrows from it - Common pattern in caches and interners
Deep Dive
How It Works
- Each reference has a lifetime region where it must stay valid
- Loans start at borrow creation, end at last use (NLL)
- Mutable loans exclude all other loans to same data
- The checker is intra-procedural plus lifetime constraints on functions
Common Error Patterns
| Error message | Typical cause |
|---|---|
| value borrowed after move | Used binding after transfer |
| cannot borrow as mutable | Active shared borrow exists |
| does not live long enough | Returned ref outlives owner |
&mut across method call | Overlapping loans in struct methods |
Fixing Strategy
- Read both error spans (borrow start and invalid use)
- Shorten borrow scope with a block
{ } - Clone data if ownership split is impossible
- Restructure to return owned values instead of references
Gotchas
- Fighting with
unsafeortransmute- Hides real design issue. Fix: Safe refactor first. - Cloning as default fix - Masks borrow API problems. Fix: Scope borrows or change return type.
- Storing references in long-lived structs - Lifetime parameters proliferate. Fix: Use owned
String/Arcor indices. - Iterator holding
&mutwhile mutating collection - Second borrow fails. Fix: Separate passes or indices. - Ignoring NLL and adding extra scopes unnecessarily - Sometimes last-use already ends loan. Fix: Trust compiler line hints.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Return owned T | Callers need independent data | Zero-copy sharing required |
| Indices into arena | Stable handles without refs | Simple tree with parent refs |
Rc<RefCell<T>> | Shared graph in one thread | Performance-critical hot path |
| Arena allocator | Many short-lived tied refs | Long-lived unrelated objects |
FAQs
Is the borrow checker a runtime check?
No. It runs at compile time only. Zero runtime cost for borrow rules.
Why is my error huge?
Rust prints full causality chains. Focus on the first error and highlighted spans.
Can the checker be too strict?
Sometimes APIs feel hard to express. Patterns like split_at_mut and lifetimes exist to model safe use.
Does it catch logic bugs?
No. Only memory safety and thread safety (with Send/Sync). Use tests for logic.
What is two-phase borrow?
Mutable borrow of struct field may be split across autoref steps in method calls. Errors can be subtle.
Polonius borrow checker?
Experimental analysis improving some error cases. Stable rustc uses NLL-based checker.
Can I disable it?
Only with unsafe Rust inside an unsafe block for raw pointers - not for normal references.
Why borrow across functions?
Lifetime parameters connect input and output borrows so callers cannot misuse returned refs.
Does `Cell` bypass the checker?
Cell moves Copy values in/out at runtime. RefCell enforces borrow rules dynamically with panics.
Best debugging tool?
rustc --explain E0499 (and related codes), rust-analyzer inlay hints, and Miri for unsafe.
Related
- Fighting the Borrow Checker - refactor patterns
- Lifetime Annotations - writing
'a - Lifetime Elision - omitted lifetimes
- Borrow Checker Error Scenarios - worked examples
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.