Fixing Common Rust Borrow Checker Errors
How do I fix common Rust borrow checker errors?
TL;DR
- Bottom line: Most borrow checker errors fall into 7 patterns (E0382, E0499, E0502, E0505, E0515, E0597, E0716) — each has a specific fix involving cloning, borrowing, restructuring scopes, or returning owned values.
- Key tool/command:
cargo check(fast compile check) andrustc --explain E0XXX - Watch out for: Reaching for
.clone()on everything — it compiles but hides design problems; prefer restructuring borrows first. - Works with: Rust 1.31+ (NLL on 2018 edition), Rust 1.63+ (NLL on all editions). Stable through Rust 1.85 (Feb 2026).
Constraints
- You can have EITHER one
&mut TOR any number of&Tto the same data at a time — never both simultaneously - A value cannot be used after its ownership moves to another variable or function call
- References must not outlive the data they point to — the compiler enforces this at every function boundary
unsafeblocks can bypass the borrow checker but shift responsibility for memory safety to the programmer — never suggestunsafeas a fix for borrow checker errors- Interior mutability types (
Cell,RefCell,Mutex) move borrow checking to runtime — panics replace compile errors if misused
Quick Reference
| # | Error Code | Cause | Likelihood | Signature | Fix |
|---|---|---|---|---|---|
| 1 | E0382 | Use of moved value | ~30% | value used here after move | Use &val (borrow), .clone(), or restructure to avoid the move |
| 2 | E0502 | Mutable borrow while immutable borrow active | ~20% | cannot borrow as mutable because it is also borrowed as immutable | End the immutable borrow before mutating; use a separate scope or reorder statements |
| 3 | E0505 | Move out of borrowed value | ~12% | cannot move out of because it is borrowed | Clone the value, or drop the borrow first |
| 4 | E0597 | Value does not live long enough | ~12% | does not live long enough | Extend the value's scope, return owned data, or add lifetime annotations |
| 5 | E0499 | Multiple mutable borrows | ~10% | cannot borrow as mutable more than once at a time | Use split_at_mut(), separate scopes, or Cell/RefCell |
| 6 | E0515 | Return reference to local variable | ~6% | cannot return reference to local variable | Return the owned value instead of a reference |
| 7 | E0716 | Temporary value dropped while borrowed | ~5% | temporary value dropped while borrowed | Bind the temporary to a named variable with let |
| 8 | E0507 | Cannot move out of borrowed content | ~3% | cannot move out of behind a shared reference | Use .clone(), std::mem::replace(), or match on ref |
| 9 | E0596 | Cannot borrow immutable as mutable | ~2% | cannot borrow as mutable, as it is not declared as mutable | Add mut to the binding: let mut x = ... |
Decision Tree
START — What does the error message say?
├── "value used here after move" (E0382)?
│ ├── Value is Copy type (i32, f64, bool, char) → Check for shadowing.
│ ├── Value used once after move → Borrow with & instead of moving.
│ ├── Value used in loop → Clone before the loop or use &val in iteration.
│ └── Value moved into closure → Use move || or clone before closure.
├── "cannot borrow as mutable because also borrowed as immutable" (E0502)?
│ ├── Immutable ref used after mutation → Reorder: finish using &ref, then &mut.
│ ├── Both refs in same expression → Extract immutable read into a let binding.
│ └── In a loop → Collect reads first, then mutate separately.
├── "cannot borrow as mutable more than once" (E0499)?
│ ├── Two &mut to same struct fields → Use split borrows or destructuring.
│ ├── In a loop → Use indices or split_at_mut().
│ └── Across function calls → Pass individual fields, not the whole struct.
├── "cannot move out of ... because it is borrowed" (E0505)?
│ ├── Drop the borrow before the move (limit scope with { }).
│ └── Clone the data before moving.
├── "does not live long enough" (E0597)?
│ ├── Returning reference to local → Return owned value.
│ ├── Storing reference in struct → Add lifetime parameter or use owned data.
│ └── Temporary in let binding → Bind temp to a longer-lived variable.
├── "cannot return reference to local variable" (E0515)?
│ └── Return the owned value: String instead of &str, Vec<T> instead of &[T].
├── "temporary value dropped while borrowed" (E0716)?
│ └── Assign the temporary to a named let binding before borrowing.
└── NONE OF THE ABOVE → Run rustc --explain EXXXX for the specific error code.
Step-by-Step Guide
1. Read the full error message
Rust's compiler errors are among the best in any language. Read the entire output — it often includes a suggested fix. [src1]
# Run a quick compile check (no codegen — faster than cargo build)
cargo check 2>&1
Verify: Look for lines starting with help: in the compiler output — these contain suggested fixes.
2. Identify the error code and look it up
Every borrow checker error has an error code (E0XXX). Use the built-in explain command to understand it. [src5]
# Get a detailed explanation with examples
rustc --explain E0382
Verify: The explanation includes a minimal example of what triggers the error and how to fix it.
3. Locate the conflicting borrows or moves
The compiler highlights the exact lines involved. Look for the "first borrow/move occurs here" and "second borrow/use occurs here" annotations. [src2]
// Example: the compiler shows exactly where the conflict is
fn main() {
let mut data = vec![1, 2, 3];
let first = &data[0]; // -- immutable borrow occurs here
data.push(4); // -- mutable borrow occurs here
println!("{}", first); // -- immutable borrow later used here
}
// Fix: finish using `first` before calling data.push()
Verify: cargo check compiles without errors after restructuring.
4. Apply the appropriate fix pattern
Based on the error code, apply the correct fix from the Quick Reference table. [src4]
// Fix pattern: Reorder to end borrow before mutation
fn main() {
let mut data = vec![1, 2, 3];
let first = data[0]; // Copy the value (i32 is Copy)
data.push(4); // Now safe: no outstanding borrows
println!("{}", first); // Uses the copied value
}
Verify: cargo check → no errors. Then cargo test to confirm behavior.
5. Run Clippy for additional suggestions
Clippy catches patterns that compile but are suboptimal (e.g., unnecessary clones). [src7]
# Run Clippy for lint-level suggestions
cargo clippy -- -W clippy::all
Verify: cargo clippy returns no warnings related to borrowing or cloning.
Code Examples
Rust: Fixing E0382 — Use of Moved Value
// Input: Code that moves a String then tries to use it
// Output: Compiling code using borrowing instead of moving
fn greet(name: &str) {
println!("Hello, {}!", name);
}
fn main() {
let user = String::from("Alice");
greet(&user); // Borrow: user is not moved
println!("Goodbye, {}!", user); // OK: user is still owned here
}
Rust: Fixing E0502 — Immutable and Mutable Borrow Conflict
// Input: Code that reads and writes to a Vec simultaneously
// Output: Compiling code with borrows properly sequenced
fn main() {
let mut scores = vec![90, 85, 78, 92];
let highest = *scores.iter().max().unwrap(); // Copy the i32 value
scores.push(95);
println!("Was: {}", highest); // Uses the copied value, not a borrow
}
Rust: Fixing E0505 — Cannot Move While Borrowed
// Input: Code that tries to move a value while a reference exists
// Output: Compiling code with the borrow scoped correctly
fn main() {
let mut data = String::from("hello");
{
let r = &data;
println!("{}", r); // Borrow used and dropped here
}
let moved = data; // Move is now safe
println!("{}", moved);
}
Rust: Fixing E0597 — Value Does Not Live Long Enough
// Input: Function returning a reference to a local value
// Output: Function returning an owned value instead
fn make_greeting(name: &str) -> String {
format!("Hello, {}!", name) // Ownership transfers to caller
}
fn main() {
let msg = make_greeting("Alice");
println!("{}", msg);
}
Anti-Patterns
Wrong: Cloning everything to silence the borrow checker
// BAD — .clone() compiles but wastes memory and hides design issues
fn process(items: &Vec<String>) {
let owned_copy = items.clone();
for item in owned_copy.iter() {
println!("{}", item);
}
}
Correct: Borrow instead of clone
// GOOD — zero-cost borrow, no allocation
fn process(items: &[String]) {
for item in items.iter() {
println!("{}", item);
}
}
Wrong: Using indices to avoid borrow checker instead of split borrows
// BAD — bypasses borrow checker with indices, prone to out-of-bounds bugs
fn swap_first_last(v: &mut Vec<i32>) {
let first = v[0];
let last = v[v.len() - 1];
v[0] = last;
v[v.len() - 1] = first;
}
Correct: Use the standard library's safe swap
// GOOD — safe, idiomatic, handles edge cases
fn swap_first_last(v: &mut Vec<i32>) {
let len = v.len();
if len >= 2 {
v.swap(0, len - 1);
}
}
Wrong: Using RefCell everywhere to avoid compile-time borrow checking
// BAD — moves borrow checking to runtime; panics if rules violated
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
let borrow1 = data.borrow();
let borrow2 = data.borrow_mut(); // PANIC at runtime!
}
Correct: Restructure code for compile-time safety
// GOOD — compile-time guarantee, no runtime panics
fn main() {
let mut data = vec![1, 2, 3];
let sum: i32 = data.iter().sum();
println!("Sum: {}", sum);
data.push(4);
println!("{:?}", data);
}
Wrong: Using unsafe to bypass the borrow checker
// BAD — undefined behavior waiting to happen
fn get_two_mut(v: &mut Vec<i32>, i: usize, j: usize) -> (&mut i32, &mut i32) {
unsafe {
let ptr = v.as_mut_ptr();
(&mut *ptr.add(i), &mut *ptr.add(j)) // UB if i == j
}
}
Correct: Use split_at_mut for safe disjoint mutable borrows
// GOOD — safe, panics on overlapping indices instead of UB
fn get_two_mut(v: &mut [i32], i: usize, j: usize) -> (&mut i32, &mut i32) {
assert!(i != j, "indices must be different");
if i < j {
let (left, right) = v.split_at_mut(j);
(&mut left[i], &mut right[0])
} else {
let (left, right) = v.split_at_mut(i);
(&mut right[0], &mut left[j])
}
}
Common Pitfalls
- Borrowing through a method call hides the borrow: Calling
self.field.method()borrows all ofself, not justfield. Fix: extract the field into a local variable first —let f = &mut self.field; f.method();. [src4] - Iterating and mutating the same collection:
for item in &vec { vec.push(...) }borrowsvecimmutably for the entire loop. Fix: collect indices or values first, then mutate in a separate pass. [src1] - Returning references to local variables: Functions cannot return references to stack-allocated locals. Fix: return owned types (
String,Vec<T>,Box<T>) or tie return references to input references via lifetimes. [src3] - Closures capture more than expected: A closure that uses
self.fieldcaptures all ofself. Fix: bindlet field = &self.field;before the closure. [src7] - Temporary values dropped too early:
let r = &some_fn().field;— the temporary is dropped at the semicolon. Fix:let tmp = some_fn(); let r = &tmp.field;. [src2] - Confusing moves with copies: Types not implementing
Copy(likeString,Vec) are moved by default. Fix: use.clone()when you genuinely need a copy, or borrow with&. [src5] - HashMap entry API ignored:
if !map.contains_key(&k) { map.insert(k, v); }borrowsmaptwice. Fix: usemap.entry(k).or_insert(v);— a single borrow. [src4] - String vs &str confusion: Passing
Stringwhere&strsuffices triggers unnecessary moves. Fix: accept&strin function signatures; Rust auto-derefs&Stringto&str. [src1]
Diagnostic Commands
# Quick compile check (no codegen — fastest feedback loop)
cargo check
# Detailed explanation for any error code
rustc --explain E0382
# Run Clippy lints (catches unnecessary clones, borrow issues)
cargo clippy -- -W clippy::all
# Run Clippy with pedantic lints for deeper analysis
cargo clippy -- -W clippy::pedantic
# Show full error output with backtrace
RUST_BACKTRACE=1 cargo check
# Check specific file without full project build
rustc --edition 2021 --crate-type lib src/module.rs 2>&1
# Use cargo expand to see macro-generated code (borrow issues in macros)
cargo expand --lib module_name
# Test with Miri for undefined behavior (nightly only)
cargo +nightly miri test
Version History & Compatibility
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Rust 1.85 (Feb 2026) | Current stable | None for borrowck | — |
| Rust 1.82 (2024 Edition) | Stable | Temporary lifetime changes in tail expressions | Some patterns with temporaries in match arms may need let bindings |
| Rust 1.63 (Aug 2022) | NLL default all editions | NLL enabled for 2015 edition | Rare: some unsound code that compiled before now rejected |
| Rust 1.36 (Jul 2019) | NLL for 2018 edition | Non-lexical lifetimes enabled | References end at last use — strictly more permissive |
| Rust 1.31 (Dec 2018) | 2018 Edition | NLL introduced (2018 edition only) | Opt-in via edition = "2018" |
| Polonius (nightly) | Experimental | More permissive borrow checking | -Zpolonius flag; resolves some false positives |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| You get any E0382/E0499/E0502/E0505/E0597/E0515/E0716 error | The error is about trait bounds (E0277) | Rust trait bounds / generics guide |
| You need to restructure code to satisfy ownership rules | The error is about lifetime annotations on function signatures | Rust lifetime annotations guide |
| You want to understand why Rust rejects your code | You are writing unsafe code intentionally | Rustonomicon (unsafe Rust reference) |
| You need to share data across threads | You need async-specific patterns (Pin, Future) | Rust async/await ownership guide |
Important Caveats
- NLL (non-lexical lifetimes) is standard since Rust 1.63 for all editions, but older blog posts and Stack Overflow answers may reference pre-NLL behavior — those fixes may be unnecessarily restrictive
- Polonius (next-gen borrow checker) is experimental on nightly (
-Zpolonius) and resolves some false positives, particularly conditional borrows in loops — it is not yet stable - The 2024 edition (Rust 1.82+) changes temporary lifetime rules in tail expressions, which may introduce new E0716 errors in code that compiled on 2021 edition
RefCell<T>andMutex<T>defer borrow checking to runtime — they are valid tools but should not be the first resort for every borrow checker error- IDE borrow checker analysis (rust-analyzer) may occasionally lag behind the actual compiler; always verify with
cargo check