Fixing Common Rust Borrow Checker Errors

Type: Software Reference Confidence: 0.93 Sources: 7 Verified: 2026-02-23 Freshness: 2026-02-23

TL;DR

Constraints

Quick Reference

#Error CodeCauseLikelihoodSignatureFix
1E0382Use of moved value~30%value used here after moveUse &val (borrow), .clone(), or restructure to avoid the move
2E0502Mutable borrow while immutable borrow active~20%cannot borrow as mutable because it is also borrowed as immutableEnd the immutable borrow before mutating; use a separate scope or reorder statements
3E0505Move out of borrowed value~12%cannot move out of because it is borrowedClone the value, or drop the borrow first
4E0597Value does not live long enough~12%does not live long enoughExtend the value's scope, return owned data, or add lifetime annotations
5E0499Multiple mutable borrows~10%cannot borrow as mutable more than once at a timeUse split_at_mut(), separate scopes, or Cell/RefCell
6E0515Return reference to local variable~6%cannot return reference to local variableReturn the owned value instead of a reference
7E0716Temporary value dropped while borrowed~5%temporary value dropped while borrowedBind the temporary to a named variable with let
8E0507Cannot move out of borrowed content~3%cannot move out of behind a shared referenceUse .clone(), std::mem::replace(), or match on ref
9E0596Cannot borrow immutable as mutable~2%cannot borrow as mutable, as it is not declared as mutableAdd 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

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

VersionStatusBreaking ChangesMigration Notes
Rust 1.85 (Feb 2026)Current stableNone for borrowck
Rust 1.82 (2024 Edition)StableTemporary lifetime changes in tail expressionsSome patterns with temporaries in match arms may need let bindings
Rust 1.63 (Aug 2022)NLL default all editionsNLL enabled for 2015 editionRare: some unsound code that compiled before now rejected
Rust 1.36 (Jul 2019)NLL for 2018 editionNon-lexical lifetimes enabledReferences end at last use — strictly more permissive
Rust 1.31 (Dec 2018)2018 EditionNLL introduced (2018 edition only)Opt-in via edition = "2018"
Polonius (nightly)ExperimentalMore permissive borrow checking-Zpolonius flag; resolves some false positives

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
You get any E0382/E0499/E0502/E0505/E0597/E0515/E0716 errorThe error is about trait bounds (E0277)Rust trait bounds / generics guide
You need to restructure code to satisfy ownership rulesThe error is about lifetime annotations on function signaturesRust lifetime annotations guide
You want to understand why Rust rejects your codeYou are writing unsafe code intentionallyRustonomicon (unsafe Rust reference)
You need to share data across threadsYou need async-specific patterns (Pin, Future)Rust async/await ownership guide

Important Caveats

Related Units