rustseverity: can-fix
E0502 / E0505 / E0596

Rust borrow checker error — cannot borrow as mutable / moved value

borrow checker: cannot borrow as mutable while borrowed

95% fixable~20 mindifficulty: intermediate

Verified against The Rust Reference: References and Borrowing, The Rust Book: Chapter 4 (Understanding Ownership), rustc error index: E0502, E0382, E0596 · Updated June 2026

> quick_fix

Rust's borrow checker enforces: at any time, you can have either one mutable reference OR any number of immutable references to the same data — never both. Restructure your code so borrows don't overlap. The error message shows exactly which borrow conflicts with which.

// Error E0502: cannot borrow `v` as mutable because it is also borrowed as immutable
let mut v = vec![1, 2, 3];
let first = &v[0];   // immutable borrow starts here
v.push(4);           // mutable borrow — ERROR: first is still alive
println!("{}", first);

// Fix — let the immutable borrow end before mutating
let mut v = vec![1, 2, 3];
let first_val = v[0];  // copy the value instead of borrowing
v.push(4);             // mutable borrow — OK, no live immutable borrows
println!("{}", first_val);

What causes this error

Rust's ownership system enforces memory safety at compile time. The borrow checker rejects code where: (1) a mutable reference exists alongside any other reference to the same data (E0502), (2) a value is used after being moved (E0382), (3) a mutable variable is borrowed immutably while also borrowed mutably (E0596), or (4) two mutable references to the same data exist simultaneously (E0499). These rules prevent data races and dangling pointers.

> advertisementAdSense placeholder

How to fix it

  1. 01

    step 1

    Read the full error message — it tells you where each borrow starts and ends

    Rust's error messages are detailed. They show the exact line of the conflicting borrows. Run `rustc --explain E0502` for a full explanation with examples for your specific error code.

    # Get detailed explanation for a specific error code
    rustc --explain E0502
    rustc --explain E0382  # use of moved value
    rustc --explain E0505  # cannot move out of because it is borrowed
    rustc --explain E0596  # cannot borrow as mutable
  2. 02

    step 2

    Narrow the scope of borrows — let them end before the conflicting operation

    The borrow checker tracks when borrows start and end. Restructure code so the immutable borrow ends (goes out of scope) before the mutable borrow begins.

    // BAD — immutable borrow of map lives across the insert
    let mut map = HashMap::new();
    map.insert("key", 1);
    let val = map.get("key");  // immutable borrow
    map.insert("other", 2);   // mutable borrow — ERROR
    println!("{:?}", val);
    
    // GOOD — copy the value out before mutating
    let mut map = HashMap::new();
    map.insert("key", 1);
    let val = map.get("key").copied();  // copy the value, end the borrow
    map.insert("other", 2);             // OK — no live borrows
    println!("{:?}", val);
  3. 03

    step 3

    Use clone() when you need to keep the data after a move

    When a value is moved into a function and you need to use it afterward, `clone()` creates an owned copy. This is a runtime cost — use it when restructuring is impractical.

    // E0382: use of moved value
    fn consume(s: String) { println!("{}", s); }  // takes ownership
    
    let s = String::from("hello");
    consume(s);          // s is moved here
    println!("{}", s);   // ERROR: use of moved value
    
    // Fix A — clone before moving
    let s = String::from("hello");
    consume(s.clone());  // clone is moved, original s still valid
    println!("{}", s);
    
    // Fix B — take a reference instead of ownership
    fn consume(s: &str) { println!("{}", s); }  // borrows instead
    let s = String::from("hello");
    consume(&s);         // borrow, s not moved
    println!("{}", s);
  4. 04

    step 4

    Use Rc<RefCell<T>> for shared mutable state (single-threaded)

    When the borrow checker structure is genuinely needed at runtime (graph nodes, tree with parent pointers), use `Rc<RefCell<T>>` for shared ownership with runtime-checked mutability. `Arc<Mutex<T>>` for multi-threaded.

    use std::rc::Rc;
    use std::cell::RefCell;
    
    let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
    let clone1 = Rc::clone(&shared);
    let clone2 = Rc::clone(&shared);
    
    clone1.borrow_mut().push(4);  // runtime borrow check
    println!("{:?}", clone2.borrow());  // [1, 2, 3, 4]

How to verify the fix

  • `cargo build` completes without borrow checker errors.
  • No unnecessary `clone()` calls — each clone should have a specific reason.
  • `cargo clippy` shows no ownership-related warnings.

Why E0502 / E0505 / E0596 happens at the runtime level

The Rust borrow checker is a static analysis pass that enforces two invariants: (1) at any point in the code, a value has exactly one owner, and (2) at any point, any number of shared references OR exactly one mutable reference may exist — never both simultaneously. These rules are enforced by tracking 'borrow regions' — the spans of code over which each reference lives. With Non-Lexical Lifetimes (NLL), the borrow ends at the last use, not the end of the lexical block. The checker runs during type inference and produces errors before code generation, preventing the entire class of memory safety bugs that plague C/C++ at zero runtime cost.

Common debug mistakes for E0502 / E0505 / E0596

  • Holding a reference from HashMap::get() while trying to insert another key — both require borrowing the map.
  • Returning a reference to a local variable — the variable is dropped at the end of the function, making the reference dangling.
  • Calling a method that takes `&mut self` while holding a `&self` reference from a previous method call.
  • Trying to mutate a captured variable in a closure that also captures it by immutable reference.
  • Storing a Vec element reference (`let r = &v[0]`) then growing the Vec with `push` — the allocation may move, invalidating the reference.

When E0502 / E0505 / E0596 signals a deeper problem

Difficulty with the borrow checker often signals that the data structure design doesn't match ownership semantics. Tree structures with parent pointers, graphs with cycles, and observer patterns with back-references are hard to express with Rust's single-owner model. The idiomatic solutions are: use indices into a Vec instead of pointers (the 'arena' pattern), use `Rc<RefCell<T>>` for single-threaded shared ownership, or restructure the algorithm to be iterator-based so references don't overlap. Crates like `petgraph` show how to implement graph algorithms idiomatically. Fighting the borrow checker on a data structure is usually a signal to redesign the structure.

Editor's take

The borrow checker is Rust's most distinctive feature and the source of most beginner frustration. The frustration is temporary but the benefit is permanent: Rust codebases don't have use-after-free bugs, double-free bugs, or data races in safe code. Every hour spent making the borrow checker happy is an hour saved debugging memory corruption at 3am in production.

The fastest path through borrow checker errors is learning to read the error messages, which are among the best in any programming language. The message tells you: where the first borrow starts, what it conflicts with, and where the conflict occurs. Running `rustc --explain E0502` gives a page-length explanation with examples. Rust's errors are so good that 'fighting the borrow checker' often means 'reading the error message carefully for the first time.'

The mental model that makes borrow checker errors click: think of references as locks. An immutable reference is a shared read lock — many can coexist. A mutable reference is an exclusive write lock — only one can exist and no read locks can coexist with it. The borrow checker is a static lock analyzer that proves your code never holds conflicting locks. When it rejects your code, it's saying: 'at this line, you're trying to acquire a write lock while already holding a read lock.' The fix is the same as with real locks: release the read lock (end the reference's lifetime) before acquiring the write lock.

By Bikram Nath · Curator · Updated June 2026

Frequently asked questions

Does the borrow checker ever accept code that could be wrong?

No — the borrow checker is sound. If it compiles, the ownership rules are satisfied and the code is free of data races, use-after-free, and dangling pointers (in safe Rust). However, it rejects some code that would be safe — this is the 'false positive' case where valid programs are rejected. The team is steadily reducing false positives with improvements like Non-Lexical Lifetimes (NLL) and Polonius.

When should I use unsafe to bypass the borrow checker?

Rarely, and only when you've verified the safety invariants manually. Common legitimate uses: FFI with C libraries, implementing data structures like linked lists, performance-critical code where you can prove safety statically. `unsafe` doesn't disable the borrow checker for safe code — it only enables 4 additional operations (raw pointer dereference, unsafe fn calls, accessing union fields, and implementing unsafe traits). Always wrap unsafe in a safe API.

Is the borrow checker getting less strict over time?

Yes, in the direction of accepting more valid programs without accepting invalid ones. Non-Lexical Lifetimes (NLL, stable since Rust 2018) made the borrow checker significantly smarter about when borrows end. Polonius (the next-generation borrow checker) will accept even more valid programs. The rules themselves — one mutable OR many immutable — will never change because they're the foundation of Rust's memory safety guarantees.

disclosure:Errordex runs AdSense, has zero third-party affiliate or sponsored links, and occasionally links to the editor’s own paid digital products (clearly labelled). Every fix is cross-referenced against the official sources listed in the “sources” sidebar before it ships. If a fix here didn’t work for you, please email so we can update the page.