Fix: Rust cannot borrow as mutable because it is also borrowed as immutable
Part of: Go, Rust & Systems Errors
Quick Answer
How to fix Rust cannot borrow as mutable error caused by aliasing rules, simultaneous references, iterator invalidation, struct method conflicts, and lifetime issues.
The Error
You compile Rust code and get:
error[E0502]: cannot borrow `data` as mutable because it is also borrowed as immutable
--> src/main.rs:5:5
|
4 | let r = &data[0];
| ---- immutable borrow occurs here
5 | data.push(4);
| ^^^^^^^^^^^^ mutable borrow occurs here
6 | println!("{}", r);
| - immutable borrow later used hereOr variations:
error[E0499]: cannot borrow `data` as mutable more than once at a timeerror[E0502]: cannot borrow `self.field` as mutable because it is also borrowed as immutableRust’s borrow checker prevents you from holding both a mutable and an immutable reference to the same data at the same time, or holding two mutable references simultaneously.
Why This Happens
Rust enforces ownership rules at compile time to prevent data races and memory safety issues. The core rule is:
- You can have either one mutable reference (
&mut) or any number of immutable references (&) to a value, but not both at the same time.
This rule exists because the compiler uses it to prove that no two pieces of code can observe a value mid-mutation. A Vec<T> may relocate its backing storage on push; any outstanding &T would then point to freed memory. A HashMap may rehash on insert; any outstanding iterator would silently observe inconsistent buckets. By forbidding the overlap at compile time, Rust eliminates an entire class of memory-safety and data-race bugs without runtime cost. The borrow checker is not being pedantic — it is preventing the program from ever reaching the configuration where the bug would happen.
This rule prevents:
- Data races. Two threads reading and writing the same data simultaneously.
- Iterator invalidation. Modifying a collection while iterating over it.
- Use-after-free. Accessing memory that has been deallocated.
The error usually shows up in three shapes. First, a long-lived &T that overlaps a later &mut — the fix is almost always to shorten the immutable borrow with Non-Lexical Lifetimes or restructure the control flow. Second, two &mut borrows of the same struct via methods — the fix is to borrow individual fields or destructure. Third, a borrow that escapes a function via a returned reference and ties the caller’s hands — the fix is to return owned data, clone, or rethink the API. Identifying which shape you are looking at is the fastest way to find the right fix.
The borrow checker enforces these rules at compile time with zero runtime cost. If your code violates them, it will not compile.
In Production: Incident Lens
This is a build-time error, so it never reaches a running production service the way a NullPointerException does. The production framing is still real — it blocks the deploy pipeline. The classic incident is “urgent customer fix, but the patch refactor trips the borrow checker, and the merge is stuck behind a 45-minute Rust compile that keeps failing.” Your error budget is being spent not on incident response but on the CI loop. Blast radius is “no new code can ship from this service until the borrow issue is restructured.” That includes unrelated fixes that happened to be queued behind the same merge train.
The monitoring signal lives in your CI dashboards: a sudden cluster of merge-blocking E0502/E0499 failures on a specific crate, paired with retries on the same PR. Wire your CI to surface the failing error code in PR summaries so reviewers can triage at a glance. Tag PRs that take more than three pushes to land — repeated borrow-checker fights are usually a sign that the underlying data layout, not the immediate patch, needs to change.
The recovery sequence is not a rollback (the bad code never landed). It is a structured response to keep the merge train moving. First, the on-call author applies the smallest local fix that compiles — clone, destructure fields, scope the borrow tighter. Second, if that fix forces an allocation in a hot path, file a follow-up issue rather than blocking the release. Third, if unsafe looks tempting, stop and pair with a second engineer; an unsafe block written under deploy pressure is the seed of next month’s CVE. The postmortem preventive is to keep cargo check and cargo clippy -- -D warnings in pre-merge CI so the borrow conflict is found locally during development rather than in the final merge gate, and to schedule periodic refactors of the data structures that keep generating this class of error.
Fix 1: Limit Reference Lifetimes
The simplest fix. Make the immutable borrow end before the mutable borrow begins:
Broken:
let mut data = vec![1, 2, 3];
let first = &data[0]; // Immutable borrow starts
data.push(4); // Mutable borrow — conflict!
println!("{}", first); // Immutable borrow still aliveFixed — use the immutable ref before mutating:
let mut data = vec![1, 2, 3];
let first = &data[0];
println!("{}", first); // Immutable borrow ends here (last use)
data.push(4); // Mutable borrow — no conflictRust uses Non-Lexical Lifetimes (NLL): a reference’s lifetime ends at its last use, not at the end of the scope. Moving the println! before push makes the immutable borrow end before the mutable borrow starts.
Pro Tip: When you get this error, look at where the immutable reference is last used. If you can move that use before the mutation, the error goes away. The borrow checker tracks the actual last usage point, not the scope boundary.
Fix 2: Clone the Data
If you need to read data and then modify the original, clone the value:
let mut data = vec![1, 2, 3];
let first = data[0]; // Copy the value (i32 implements Copy)
data.push(4); // No conflict — first is a copy, not a reference
println!("{}", first);For types that do not implement Copy:
let mut names = vec!["Alice".to_string(), "Bob".to_string()];
let first = names[0].clone(); // Clone the String
names.push("Charlie".to_string());
println!("{}", first); // Uses the clone, not a referenceTrade-off: Cloning allocates new memory. For large data or hot loops, consider restructuring instead.
Fix 3: Use Indices Instead of References
Avoid holding references to collection elements by using indices:
Broken:
let mut items = vec![1, 2, 3];
let item_ref = &items[0];
items.push(4); // Error!
println!("{}", item_ref);Fixed — use index:
let mut items = vec![1, 2, 3];
let idx = 0;
items.push(4);
println!("{}", items[idx]); // Access by index after mutationIndices are plain usize values — they do not borrow the collection.
Fix 4: Fix Struct Method Borrowing
A common issue when a struct method borrows &self and you want to modify a field:
Broken:
struct Game {
players: Vec<String>,
scores: Vec<i32>,
}
impl Game {
fn get_player(&self, idx: usize) -> &str {
&self.players[idx]
}
fn update_score(&mut self, idx: usize, score: i32) {
self.scores[idx] = score;
}
fn process(&mut self) {
let name = self.get_player(0); // Borrows &self (immutable)
self.update_score(0, 100); // Borrows &mut self — conflict!
println!("{}", name);
}
}Fixed — clone to break the borrow:
fn process(&mut self) {
let name = self.get_player(0).to_string(); // Clone breaks the borrow
self.update_score(0, 100);
println!("{}", name);
}Fixed — borrow fields separately:
fn process(&mut self) {
let name = &self.players[0]; // Borrows only self.players
self.scores[0] = 100; // Mutates only self.scores — no conflict!
println!("{}", name);
}Rust can track borrows of individual fields when you access them directly (not through methods). Method calls borrow the entire self.
Common Mistake: Using methods like
self.get_player()when direct field accessself.players[0]would avoid the borrow conflict. Methods borrow all ofself; field access borrows only that specific field.
Fix 5: Use Interior Mutability (RefCell, Cell)
When you need to mutate data behind an immutable reference, use interior mutability:
RefCell (runtime borrow checking):
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
let borrowed = data.borrow(); // Immutable borrow
println!("{:?}", *borrowed);
drop(borrowed); // Must drop before mutable borrow
data.borrow_mut().push(4); // Mutable borrowCell (for Copy types):
use std::cell::Cell;
let counter = Cell::new(0);
counter.set(counter.get() + 1); // Mutate without &mutRefCell trade-off: Borrow rules are checked at runtime instead of compile time. Violating them causes a panic (BorrowMutError) instead of a compile error.
Fix 6: Fix Iterator and Loop Mutations
You cannot modify a collection while iterating over it:
Broken:
let mut items = vec![1, 2, 3, 4, 5];
for item in &items {
if *item > 3 {
items.retain(|&x| x <= 3); // Mutates during iteration!
}
}Fixed — collect modifications, apply after:
let mut items = vec![1, 2, 3, 4, 5];
items.retain(|&x| x <= 3); // Filter in one passFixed — use indices:
let mut items = vec![1, 2, 3, 4, 5];
let mut i = 0;
while i < items.len() {
if items[i] > 3 {
items.remove(i);
} else {
i += 1;
}
}Fixed — build a new collection:
let items = vec![1, 2, 3, 4, 5];
let filtered: Vec<_> = items.into_iter().filter(|&x| x <= 3).collect();For more on Rust’s borrow checker fundamentals, see Fix: Rust borrow checker error.
Fix 7: Use Mutex for Multi-Threaded Access
For shared mutable state across threads:
use std::sync::{Arc, Mutex};
use std::thread;
let data = Arc::new(Mutex::new(vec![1, 2, 3]));
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut locked = data_clone.lock().unwrap();
locked.push(4);
});
handle.join().unwrap();
println!("{:?}", data.lock().unwrap());RwLock for read-heavy workloads:
use std::sync::RwLock;
let data = RwLock::new(vec![1, 2, 3]);
// Multiple concurrent readers:
let read1 = data.read().unwrap();
let read2 = data.read().unwrap(); // OK — multiple reads allowed
drop(read1);
drop(read2);
// Exclusive writer:
let mut write = data.write().unwrap();
write.push(4);Fix 8: Split Borrows with Destructuring
When the borrow checker complains about borrowing parts of a struct:
struct State {
input: Vec<String>,
output: Vec<String>,
}
impl State {
fn process(&mut self) {
// Destructure to split the borrow:
let State { input, output } = self;
for item in input.iter() {
output.push(item.to_uppercase());
}
}
}Destructuring tells the compiler that input and output are separate fields, allowing simultaneous immutable borrow of input and mutable borrow of output.
Still Not Working?
Use Rc<RefCell<T>> for shared ownership with mutation:
use std::rc::Rc;
use std::cell::RefCell;
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let clone = Rc::clone(&shared);
shared.borrow_mut().push(4);
println!("{:?}", clone.borrow());Consider unsafe as a last resort. Raw pointers bypass the borrow checker. Only use this when you are certain about memory safety and the safe alternatives are not viable:
let mut data = vec![1, 2, 3];
let ptr = data.as_mut_ptr();
unsafe {
*ptr = 10; // Direct memory access
}Warning: unsafe does not turn off the borrow checker rules — it transfers the burden of proof from the compiler to you. A bug in an unsafe block produces undefined behavior, not a panic, which means production crashes that are impossible to reproduce locally.
Check for lifetime annotation issues. Sometimes adding explicit lifetime annotations helps the compiler understand your intentions:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}If you keep adding lifetimes and the compiler keeps rejecting your code, the real issue is probably that you are returning a reference to a local value. See Fix: Rust lifetime error for the common patterns.
Consider restructuring your data. If you frequently hit borrow checker issues with a specific data structure, the structure might not fit Rust’s ownership model. Consider using entity-component systems, arena allocators, or different data layouts.
Audit any &mut self method that calls other self methods. Once a method holds &mut self, it cannot call any other method that takes &self, because the mutable borrow is exclusive. Refactor the helper into a free function that takes the specific fields it needs, or inline the helper into the caller. This single refactoring resolves a surprising number of E0502 cases.
Try cargo check instead of cargo build while iterating. It skips code generation and reports borrow errors in a fraction of the time. In a CI environment under deploy pressure, running cargo check as a separate fast-failing job ahead of cargo test saves minutes per attempt.
Beware of trait-related conflicts. If you implement Deref on a wrapper type, the compiler will automatically use it during method resolution. A method called through Deref borrows the inner type, not the wrapper, which can produce confusing E0502 errors that look unrelated to the visible code. If the trait itself is not implemented, see Fix: Rust trait not implemented — the same trait resolution rules apply.
If the underlying compilation problem is an unhandled Result or Option pattern instead of a borrow conflict, see Fix: Rust error handling not working. For general Rust compilation errors, the Rust compiler’s error messages are unusually helpful — read the full output including the suggestions. The compiler often tells you exactly how to fix the issue.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Tauri Not Working — Command Not Found, IPC Error, File System Permission Denied, or Build Fails
How to fix Tauri app issues — Rust command registration, invoke IPC, tauri.conf.json permissions, fs scope, window management, and common build errors on Windows/macOS/Linux.
Fix: Rust Error Handling Not Working — ? Operator, Custom Error Types, and thiserror/anyhow
How to fix Rust error handling issues — the ? operator, From trait for error conversion, thiserror for custom errors, anyhow for applications, and Box<dyn Error> pitfalls.
Fix: Rust lifetime may not live long enough / missing lifetime specifier
How to fix Rust lifetime errors including missing lifetime specifier, may not live long enough, borrowed value does not live long enough, and dangling references.
Fix: Rust the trait X is not implemented for Y (E0277)
How to fix Rust compiler error E0277 'the trait X is not implemented for Y' with solutions for derive macros, manual trait implementations, Send/Sync bounds, trait objects, and generics.