cargo check (fast compile check) and rustc --explain E0XXX.clone() on everything — it compiles but hides design problems; prefer restructuring borrows first.&mut T OR any number of &T to the same data at a time — never both simultaneouslyunsafe blocks can bypass the borrow checker but shift responsibility for memory safety to the programmer — never suggest unsafe as a fix for borrow checker errorsCell, RefCell, Mutex) move borrow checking to runtime — panics replace compile errors if misused| # | 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 = ... |
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.
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.
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.
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.
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.
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.
// 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
}
// 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
}
// 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);
}
// 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);
}
// 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);
}
}
// GOOD — zero-cost borrow, no allocation
fn process(items: &[String]) {
for item in items.iter() {
println!("{}", item);
}
}
// 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;
}
// 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);
}
}
// 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!
}
// 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);
}
// 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
}
}
// 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])
}
}
self.field.method() borrows all of self, not just field. Fix: extract the field into a local variable first — let f = &mut self.field; f.method();. [src4]for item in &vec { vec.push(...) } borrows vec immutably for the entire loop. Fix: collect indices or values first, then mutate in a separate pass. [src1]String, Vec<T>, Box<T>) or tie return references to input references via lifetimes. [src3]self.field captures all of self. Fix: bind let field = &self.field; before the closure. [src7]let r = &some_fn().field; — the temporary is dropped at the semicolon. Fix: let tmp = some_fn(); let r = &tmp.field;. [src2]Copy (like String, Vec) are moved by default. Fix: use .clone() when you genuinely need a copy, or borrow with &. [src5]if !map.contains_key(&k) { map.insert(k, v); } borrows map twice. Fix: use map.entry(k).or_insert(v); — a single borrow. [src4]String where &str suffices triggers unnecessary moves. Fix: accept &str in function signatures; Rust auto-derefs &String to &str. [src1]# 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 | 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 |
| 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 |
-Zpolonius) and resolves some false positives, particularly conditional borrows in loops — it is not yet stableRefCell<T> and Mutex<T> defer borrow checking to runtime — they are valid tools but should not be the first resort for every borrow checker errorcargo check