If you have written more than a few lines of Rust, you have seen a borrow checker error. The borrow checker is the part of the compiler that enforces Rust’s ownership rules before your program runs. It tracks which binding owns each value, meaning which variable name is responsible for freeing the value’s memory. It also checks that no reference outlives the data it points to and that you never read and write the same data at the same time through overlapping references. A reference is a pointer-like value that lets you read or write data without owning it.
The rules the compiler enforces are short:
- Each value has one owner.
- You can hold any number of shared references, which allow reading only, as long as no mutable reference exists.
- You can hold one mutable reference that allows reading and writing. While it exists, no other reference to the same data may be used.
- A reference must not outlive the value it points to.
When your code breaks one of these rules, the compiler stops and prints an error code such as E0502 or E0597. The code tells you which rule you broke, and the message points at the exact lines involved. Modern compiler messages are built around your code: primary labels explain what the compiler objects to, and secondary labels explain why. This format was introduced in the RFC that redesigned compiler errors to be readable by people who are not compiler experts.
Most borrow checker errors fall into a few patterns. Once you recognize the pattern, the fix is usually mechanical: copy a value instead of borrowing it, end one borrow before starting another, add an explicit scope, or restructure the code so the borrow’s lifetime ends earlier. The sections that follow cover the errors that appear most often in real code. Each one shows the broken code, explains why the compiler rejects it, and gives a working rewrite.
Moves: using a value after ownership has transferred (E0382)
In Rust, passing a value to a function by value moves it: the function becomes the new owner, and the original binding is no longer valid. A move is cheap because Rust only copies the fields that live on the stack, such as a string’s pointer, length, and capacity; the heap data behind the pointer is not duplicated. After a move, using the original binding is an error.
fn main() {
let text = String::from("hello");
print_text(text);
println!("{}", text); // error[E0382]: borrow of moved value: `text`
}
fn print_text(text: String) {
println!("{}", text);
}
String is heap-allocated and does not implement the Copy trait. The Copy trait tells Rust that a value can be duplicated by copying its bytes. Because String owns heap memory, duplicating it would require deciding which copy owns that memory, so Rust requires it to be moved or borrowed. Types like i32 and bool implement Copy; types like String and Vec<T> do not, which is why a Vec cannot be used after it is moved.
The fix depends on whether the function needs to own the data. If it only needs to read it, pass a reference instead of the value:
fn main() {
let text = String::from("hello");
print_text(&text);
println!("{}", text);
}
fn print_text(text: &String) {
println!("{}", text);
}
If the function must own the data and you still need the original afterward, clone the value before passing it. Cloning duplicates the heap data, so use it deliberately rather than as a default reaction to a borrow error. The underlying mechanism is the same in both cases: a move is a shallow copy of the stack fields, not a deep copy of everything the value owns.
Forgetting mut on a binding (E0596)
The simplest borrow checker error is also one of the most common. To create a mutable reference to a variable, the variable itself must be declared mutable with mut.
fn main() {
let count = 0;
let counter = &mut count; // error[E0596]: cannot borrow `count` as mutable, as it is not declared as mutable
*counter += 1;
}
The compiler will not let you take a mutable reference to an immutable binding, because the binding’s immutability is part of the contract that other code relies on. The fix is to add mut to the declaration:
fn main() {
let mut count = 0;
let counter = &mut count;
*counter += 1;
println!("{}", count);
}
A mutable borrow while an immutable borrow is still alive (E0502)
This error appears when you hold a shared reference and then try to mutate the same data while that reference is still usable. The compiler must guarantee that data does not change underneath a reference, because a reference is conceptually a pointer: any write through another path could invalidate what the reference sees.
fn main() {
let mut numbers = vec![1, 2, 3];
let first = &numbers[0];
numbers.push(4); // error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
println!("{}", first);
}
The immutable borrow first is still in use at the push, so the mutable borrow is rejected. The fix is to make sure the immutable borrow ends before the mutation. One way is to end the immutable borrow before the mutable borrow by moving the read ahead of the mutation:
fn main() {
let mut numbers = vec![1, 2, 3];
let first = &numbers[0];
println!("{}", first);
numbers.push(4);
}
Another way is to copy the value out instead of borrowing it. Because i32 implements Copy, reading numbers[0] copies the number and no borrow is held:
fn main() {
let mut numbers = vec![1, 2, 3];
let first = numbers[0];
numbers.push(4);
println!("{}", first);
}
A copy creates independent data, so it can coexist with a mutable reference to the original; a borrow cannot. Whether the code passes a pointer or copies the value determines which of these errors appear.
Two mutable borrows of the same value (E0499)
A mutable reference gives you exclusive access. While it exists, creating a second mutable reference to the same data is an error, even if you do not use the first one again.
fn main() {
let mut value = 5;
let a = &mut value;
let b = &mut value; // error[E0499]: cannot borrow `value` as mutable more than once at a time
*a += 1;
*b += 1;
}
The compiler cannot know that you will use a and b at different times, and two live mutable references would let two parts of the program write the same memory without coordination. The fix is to end the first borrow before starting the second, either by using a first and letting it go out of scope, or by putting the first borrow in an explicit block:
fn main() {
let mut value = 5;
{
let a = &mut value;
*a += 1;
}
let b = &mut value;
*b += 1;
println!("{}", value);
}
The explicit block makes the end of the first borrow visible to you and the compiler. When you need to mutate several elements of one collection, there are better tools than sequential borrows, covered later in this article.
Dangling references: value dropped while still borrowed (E0597)
E0597 is the compiler catching a dangling reference, a reference that points to memory whose value has already been destroyed. In languages without a borrow checker, this kind of bug causes use-after-free crashes, so Rust treats it as a compile-time error.
fn main() {
let r;
{
let x = 5;
r = &x; // error[E0597]: `x` does not live long enough
}
println!("{}", r);
}
The inner block creates x, and r points to it. At the end of the block, x is dropped, but r is still in scope and still expected to hold a valid value. The official documentation for E0597 shows the same shape of example and the same fix: remove the inner scope so the borrowed value lives until after the last use.
fn main() {
let x = 5;
let r = &x;
println!("{}", r);
}
Drop order produces the same error in a subtler form. Values in a scope are dropped in the reverse order of their declaration. If the value holding a reference is declared before the value it points to, the pointed-to value drops first at the end of the scope, while the reference holder still exists. The fix is to declare the borrowed value first, or drop the reference holder explicitly with drop(holder) before the end of the scope. The same ordering shows up in APIs: in wgpu, a RenderPass borrows its CommandEncoder, and you must finish or drop the render pass before moving the encoder into a submit call, otherwise the compiler rejects the move while the render pass is still alive.
Moving a value out from behind a reference (E0507)
A shared reference lets you read what it points to, but it does not give you ownership. Moving the value out of the reference is an error because the original owner would be left with nothing.
fn main() {
let text = String::from("hello");
let borrowed = &text;
let owned = *borrowed; // error[E0507]: cannot move out of `*borrowed` which is behind a shared reference
}
For Copy types this code would work, because reading the value copies it instead of moving it. String does not implement Copy, so dereferencing the shared reference attempts a move, and the compiler rejects it. The fix is to clone the value, producing an owned copy of the heap data:
fn main() {
let text = String::from("hello");
let borrowed = &text;
let owned = borrowed.clone();
}
Cloning is more expensive than borrowing because it duplicates the heap allocation, so use it when you genuinely need an owned value and cannot change how the reference is created. The readable error format redesign targeted messages like "cannot move out of borrowed content": earlier versions of the compiler used terminology that was opaque to new users, while current versions label the exact value and operation involved.
Borrow conflicts inside loops (E0502)
Loops produce borrow checker errors that look wrong at first, because the borrow the loop itself creates stays alive for the entire loop body.
fn main() {
let mut numbers = vec![1, 2, 3];
for n in &numbers {
numbers.push(4); // error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
}
}
The for n in &numbers line borrows numbers immutably for the whole loop, so every iteration of the body runs with a live immutable borrow. The mutation inside is rejected no matter how many times the loop would run. The fix is to stop borrowing the collection while mutating it. Collect the values you need before the loop and mutate the original afterward, or iterate over indices instead of references, so the borrow from indexing lasts only for the duration of each statement:
fn main() {
let mut numbers = vec![1, 2, 3];
for i in 0..numbers.len() {
numbers[i] = numbers[i] * 2;
}
println!("{:?}", numbers);
}
A related technique for loop cases where the compiler treats per-iteration borrows as overlapping is to hold one mutable reference across all iterations instead of creating a new borrow each time. And when a loop returns a borrow and the code then needs to mutate the same data, the reliable workaround is to break out of the loop, perform the mutation, and create a fresh borrow afterward, so the old borrow is provably dead before the mutation starts.
When the compiler rejects code that looks correct
Some borrow checker errors survive even when a human can see the borrow is no longer used. The cause is a known limitation in how the compiler tracks lifetimes. A named lifetime, the annotation that ties a reference to the data it points to, is considered to last until the end of the function across every code path, even if the reference is dropped early or the function returns before the reference is used again. This limitation, which produces errors like E0502 in loops and early-return code, is the target of a long-running compiler project called Polonius.
The most common real-world instance is a lookup-then-insert into a hash map:
use std::collections::HashMap;
fn get_or_insert(map: &mut HashMap<String, String>, key: &str) -> &String {
match map.get(key) {
Some(value) => value,
None => {
map.insert(key.to_string(), String::new()); // error[E0502]
&map[key]
}
}
}
In the None arm, the reference from map.get(key) does not exist; the call returned None. But the compiler treats the immutable borrow of map as live across the whole match, so the insert is rejected. The entry API handles this pattern by returning a mutable reference to the value, inserting a default first if the key is missing, so no borrow spans the insert:
use std::collections::HashMap;
fn get_or_insert(map: &mut HashMap<String, String>, key: &str) -> &String {
map.entry(key.to_string()).or_insert_with(String::new)
}
Polonius aims to make borrows end exactly at their last use, which would accept the first version. Until that lands in stable Rust, restructuring the code or using the API designed for the pattern is the fix. For data structures that need to be mutated through shared references, RefCell<T> moves the borrow check from compile time to runtime: it allows multiple immutable borrows or one mutable borrow and panics if the rules are violated at runtime. Treat RefCell as a deliberate escape hatch for cases where the borrow pattern is correct but the compiler cannot prove it, not as a way to silence errors.
Mutating different parts of one collection
A common real need is mutating two elements of the same collection at the same time. You cannot do it by writing two indexing expressions, because each &mut numbers[i] borrows the whole collection.
fn main() {
let mut numbers = vec![1, 2, 3, 4];
let a = &mut numbers[0];
let b = &mut numbers[1]; // error[E0499]: cannot borrow `numbers` as mutable more than once
}
The standard fix is to split the slice. split_at_mut divides a slice into two disjoint parts, each of which gets its own mutable borrow, because the compiler can prove the parts do not overlap:
fn main() {
let mut numbers = vec![1, 2, 3, 4];
let (left, right) = numbers.split_at_mut(2);
left[0] = 10;
right[0] = 20;
println!("{:?}", numbers);
}
A related approach is to pass indices around instead of mutable references: the function that does the mutation takes the collection and the index, so the borrow is created and released inside that function. This is the pattern the Rust users forum recommends when you need to mutate multiple elements without splitting the slice.
Borrowing different fields of a struct is allowed for the same reason splitting a slice is: the compiler can see the fields are disjoint memory. But calling a method that borrows the whole struct stops you from borrowing a second field, because the method’s &mut self covers everything:
struct Point {
x: i32,
y: i32,
}
impl Point {
fn x_mut(&mut self) -> &mut i32 {
&mut self.x
}
}
fn main() {
let mut p = Point { x: 0, y: 0 };
let x = p.x_mut();
let y = &mut p.y; // error[E0499]: cannot borrow `p` as mutable more than once
}
Accessing the fields directly with let x = &mut p.x; lets both borrows coexist, because the compiler tracks each field separately.