Rust borrow checker error — cannot borrow as mutable / moved value
borrow checker: cannot borrow as mutable while borrowed
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.
How to fix it
- 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 - 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); - 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); - 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.