Rust Borrow Checker: Rules & Common Errors Explained

Max WellsMax WellsFounder of Rustify

TL;DR: The borrow checker is the part of the Rust compiler that enforces rules around references. You can have either one mutable reference OR any number of immutable references to a value at a time; never both simultaneously. This eliminates data races and dangling pointers at compile time with no runtime cost.


What Is the Borrow Checker?

The borrow checker is a component of the Rust compiler (rustc) that tracks how references to values are used and rejects programs that violate memory safety rules; before the program ever runs.

The term "borrow" comes from the analogy: instead of taking ownership of a value, you borrow it temporarily. The borrow checker ensures that borrowed references never outlive the data they point to, and that mutable and immutable borrows don't coexist.

Every Rust program that compiles is guaranteed to be free of:

  • Dangling pointers (references to freed memory)
  • Data races (concurrent unsynchronized mutation)
  • Use-after-free bugs

What Are the Rules of Borrowing?

Rust enforces two fundamental borrowing rules at compile time:

  1. At any given time, you can have either one mutable reference or any number of immutable references; not both
  2. References must always be valid; they cannot outlive the data they point to
fn main() {
    let mut s = String::from("hello");
 
    let r1 = &s;     // immutable borrow ✅
    let r2 = &s;     // second immutable borrow ✅ (allowed)
    println!("{r1} and {r2}");
 
    let r3 = &mut s; // mutable borrow ✅ (r1 and r2 no longer used)
    r3.push_str(", world");
    println!("{r3}");
}

This would fail:

fn main() {
    let mut s = String::from("hello");
 
    let r1 = &s;      // immutable borrow
    let r2 = &mut s;  // ❌ cannot borrow as mutable while immutable borrow exists
 
    println!("{r1}");
}

Why Does the Borrow Checker Exist?

The borrow checker exists to eliminate at compile time the entire class of memory bugs that make C and C++ programs vulnerable; there is no runtime overhead.

Consider a classic use-after-free bug in C:

char *ptr = malloc(10);
free(ptr);
strcpy(ptr, "oops"); // undefined behavior (ptr points to freed memory)

The Rust borrow checker makes this impossible. If you free a value (by dropping its owner), all references to it become invalid; the compiler will refuse to compile code that uses them.


What Are the Most Common Borrow Checker Errors?

The three most common errors are: simultaneous mutable and immutable borrows, use after move, and references that outlive their data.

1. Simultaneous mutable + immutable borrow:

let mut v = vec![1, 2, 3];
let first = &v[0];   // immutable borrow
v.push(4);           // ❌ mutable borrow (vec may reallocate, invalidating `first`)
println!("{first}");

2. Use after move:

let s = String::from("hello");
let s2 = s;          // s is moved
println!("{s}");     // ❌ s no longer owns the value

3. Reference outlives data (dangling reference):

fn dangle() -> &String {    // ❌ returns reference to local variable
    let s = String::from("hello");
    &s
} // s is dropped here (the reference would be dangling)

How Do You Work With the Borrow Checker Effectively?

The key insight is to treat the borrow checker as a collaborator, not an obstacle; its errors tell you exactly where your mental model of data ownership is wrong.

Practical strategies:

  • Shorten borrow scopes: immutable borrows end when last used, not at end of block
  • Clone when in doubt: cheap during development; optimize later if needed
  • Use owned types in structs: avoid lifetime annotations until you understand them
  • Read the full error message: rustc errors include a "help" suggestion ~80% of the time
// Instead of fighting the borrow checker:
fn process(data: &Vec<i32>) -> i32 {
    data.iter().sum()
}
 
// Pass references for read-only access:
let numbers = vec![1, 2, 3];
let total = process(&numbers);  // borrow, don't move
println!("Sum: {total}, data: {numbers:?}"); // ✅ numbers still valid

Frequently Asked Questions

Yes, for most developers. The "borrow checker wall" (a period of 1–3 weeks where the compiler rejects programs whose intent seems correct) is where ~80% of Rust learners quit. The breakthrough comes when you stop fighting it and start thinking about why it's rejecting the code.

No. All borrow checking happens at compile time. There are zero runtime checks, no reference counting, and no garbage collection. This is one of Rust's key performance advantages.

NLL is a borrow checker improvement introduced in Rust 2018 that made borrow scopes end when a reference is last used rather than at the end of the enclosing block. It eliminated many false-positive errors that made the borrow checker unnecessarily restrictive.

Yes, with unsafe blocks, but only for advanced use cases (FFI, custom data structures). This shifts responsibility for safety to the programmer. The vast majority of Rust code never needs unsafe.


Sources


  • Ownership: The system the borrow checker enforces
  • Lifetime: How long references remain valid
  • Mutex: Safe shared mutation across threads
  • Slice: Borrowed slices are one of the most common places borrow-checker rules show up

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?