Fix: Rust Borrow Checker Errors – Cannot Borrow as Mutable, Value Moved, and Lifetimes
Quick Answer
How to fix common Rust borrow checker errors including 'cannot borrow as mutable', 'value used after move', and lifetime annotation issues.
The Error
You try to compile your Rust program and the borrow checker rejects it with one of these errors:
Cannot borrow as mutable — multiple mutable references:
error[E0499]: cannot borrow `data` as mutable more than once at a time
--> src/main.rs:4:17
|
3 | let r1 = &mut data;
| --------- first mutable borrow occurs here
4 | let r2 = &mut data;
| ^^^^^^^^^ second mutable borrow occurs here
5 | println!("{}, {}", r1, r2);
| -- first borrow later used hereValue used after being moved:
error[E0382]: borrow of moved value: `name`
--> src/main.rs:4:20
|
2 | let name = String::from("Alice");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
3 | let greeting = format!("Hello, {}", name);
| ---- value moved here
4 | println!("{}", name);
| ^^^^ value borrowed here after moveMissing lifetime specifier:
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn first_word(s: &str) -> &str {
| ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say which one of the input lifetimes it is borrowed fromNone of these are warnings. Rust will not compile your code until every borrow checker violation is resolved. This is by design — the compiler guarantees memory safety at compile time with zero runtime cost.
Why This Happens
Rust enforces memory safety without a garbage collector through its ownership system. Three rules govern how values are created, shared, and destroyed:
- Each value has exactly one owner. When the owner goes out of scope, the value is dropped and its memory is freed.
- At any given time, you can have either one mutable reference (
&mut T) or any number of immutable references (&T) — but not both simultaneously. - Every reference must be valid. A reference cannot outlive the data it points to.
The borrow checker enforces these rules at compile time. There is no garbage collector, no reference counting overhead (unless you opt into it), and no possibility of dangling pointers or data races in safe Rust.
These errors appear most often in these situations:
- Multiple mutable references. You try to create two
&mutreferences to the same data at the same time. - Using a value after it has been moved. Assigning a
String,Vec, or any non-Copytype to another variable transfers ownership. The original variable is no longer valid. - Returning a reference to local data. A function creates a value on the stack and tries to return a reference to it. The value is dropped when the function returns, leaving a dangling reference.
- Iterating and modifying a collection. You loop over a
Vecand try to push or remove elements inside the loop body. - Lifetime ambiguity. The compiler cannot determine how long a returned reference should live when multiple input references are involved.
Understanding the root cause is essential. Each fix below targets a specific pattern. Pick the one that matches your situation.
Fix 1: Eliminate Overlapping Mutable Borrows
The most direct “cannot borrow as mutable” error comes from having two &mut references alive at the same time. Rust forbids this because two mutable references to the same data enable data races.
Broken — two mutable references overlap:
let mut scores = vec![10, 20, 30];
let first = &mut scores[0];
scores.push(40); // second mutable borrow of scores
*first += 1; // first borrow still in use hereFixed — finish using the first borrow before creating the second:
let mut scores = vec![10, 20, 30];
let first = &mut scores[0];
*first += 1; // first borrow's last use
scores.push(40); // no conflict — first borrow is doneSince Rust 2018, the compiler uses non-lexical lifetimes (NLL). A borrow is considered active until its last use, not until the end of the enclosing block. This means you can often fix borrow conflicts simply by reordering lines so that one borrow ends before the next begins.
Before NLL, the following code would fail because both borrows lived until the closing brace:
let mut data = vec![1, 2, 3];
let r = &data[0];
println!("{}", r); // last use of r
data.push(4); // works with NLL — r's borrow ended at printlnIf you are working with an older Rust edition (pre-2018), upgrading your Cargo.toml to edition = "2021" enables NLL and resolves many borrow conflicts automatically.
Fix 2: Clone the Value to Avoid Move Errors
When you assign a non-Copy value to a new variable or pass it to a function, ownership is transferred. The original variable becomes invalid. If you need to keep using the original, clone it.
Broken — value moved:
let cities = vec!["Paris", "Tokyo", "Lima"];
let backup = cities;
println!("{:?}", cities); // error: value used after moveFixed — clone to create an independent copy:
let cities = vec!["Paris", "Tokyo", "Lima"];
let backup = cities.clone();
println!("{:?}", cities); // works — cities still owns the original
println!("{:?}", backup); // backup is an independent copyCloning is appropriate when:
- The data is small and the performance cost is negligible.
- You genuinely need two independent copies that can diverge.
- You are prototyping and want to get the code compiling before optimizing.
Cloning is wasteful when:
- A reference (
&T) would serve the same purpose. - You are cloning large collections in a hot loop.
- You clone reflexively to silence every borrow checker error without understanding the underlying issue.
Clone is a legitimate tool in Rust’s ownership model, not a code smell. But when you find yourself cloning everything, it is a signal to learn references and borrowing more deeply. Similar patterns appear in other languages — for instance, understanding when values are copied versus referenced helps with TypeScript type errors as well.
Fix 3: Use References Instead of Taking Ownership
If a function only needs to read data, pass a reference instead of transferring ownership. This is the most common and idiomatic fix for move errors.
Broken — function consumes the value:
fn count_words(text: String) -> usize {
text.split_whitespace().count()
}
let article = String::from("Rust is fast and safe");
let count = count_words(article);
println!("{}", article); // error: value used after moveFixed — function borrows the value:
fn count_words(text: &str) -> usize {
text.split_whitespace().count()
}
let article = String::from("Rust is fast and safe");
let count = count_words(&article);
println!("{}", article); // works — article was only borrowedDesign guidelines for function signatures:
- Use
&Twhen the function only reads. - Use
&mut Twhen the function needs to modify in place. - Take
Tby value only when the function must own the data (storing it in a struct, consuming it, or returning a transformed version).
This principle extends to method receivers. Use &self for read-only methods and &mut self for mutating methods. Taking self (by value) should be reserved for methods that consume the object, like builder patterns or .into_*() conversions.
Fix 4: Use Rc and Arc for Shared Ownership
Sometimes multiple parts of your program need to own the same data. A single owner with references does not work because the references would need complex lifetimes. In these cases, use reference-counted smart pointers.
Rc<T> for single-threaded shared ownership:
use std::rc::Rc;
let config = Rc::new(String::from("production"));
let handler_a = Rc::clone(&config);
let handler_b = Rc::clone(&config);
println!("A uses: {}", handler_a);
println!("B uses: {}", handler_b);Rc::clone does not deep-copy the string. It increments a reference count. The data is freed when the last Rc is dropped.
Arc<T> for multi-threaded shared ownership:
use std::sync::Arc;
use std::thread;
let config = Arc::new(String::from("production"));
let config_clone = Arc::clone(&config);
let handle = thread::spawn(move || {
println!("Thread uses: {}", config_clone);
});
println!("Main uses: {}", config);
handle.join().unwrap();Arc is identical to Rc but uses atomic operations for thread safety. It is slightly more expensive, so use Rc when you know the data stays on one thread.
Choose the right tool:
| Scenario | Type |
|---|---|
| Single owner | T |
| Multiple readers, single thread | Rc<T> |
| Multiple readers, multiple threads | Arc<T> |
| Shared + mutable, single thread | Rc<RefCell<T>> |
| Shared + mutable, multiple threads | Arc<Mutex<T>> |
Pro Tip: When you’re first learning Rust, it’s perfectly fine to use
.clone()to get past the borrow checker and ship working code. Come back and optimize later once you understand ownership patterns better. Premature optimization of ownership is one of the biggest productivity killers for Rust beginners.
Fix 5: Interior Mutability with RefCell and Mutex
Rc and Arc give shared ownership but the inner data is immutable. When you need shared ownership and mutation, use interior mutability.
RefCell<T> — runtime borrow checking (single-threaded):
use std::cell::RefCell;
use std::rc::Rc;
let shared_list = Rc::new(RefCell::new(vec![1, 2, 3]));
let alias = Rc::clone(&shared_list);
shared_list.borrow_mut().push(4);
alias.borrow_mut().push(5);
println!("{:?}", shared_list.borrow()); // [1, 2, 3, 4, 5]RefCell moves borrow checking from compile time to runtime. If you call borrow_mut() while another borrow_mut() or borrow() is active, the program panics. This is a trade-off: you gain flexibility but lose the compile-time guarantee.
Mutex<T> — thread-safe interior mutability:
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut val = counter.lock().unwrap();
*val += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", *counter.lock().unwrap()); // 5Mutex blocks the current thread until the lock is available. If you need a non-blocking alternative, consider RwLock<T> (multiple readers, one writer) or tokio::sync::Mutex for async code.
A word of caution: interior mutability is powerful but can hide bugs that the borrow checker would normally catch. Use it when the ownership model genuinely cannot express what you need — not as a blanket workaround.
Fix 6: Add Lifetime Annotations
When a function returns a reference, the compiler needs to know how long that reference is valid. If there is only one input reference, Rust infers the lifetime automatically (this is called lifetime elision). When there are multiple input references, you must annotate explicitly.
Broken — ambiguous lifetime:
fn longer<'a>(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}Fixed — explicit lifetime annotation:
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}The 'a annotation means: “the returned reference lives at least as long as the shorter of the two input lifetimes.” This gives the compiler enough information to verify that the caller does not use the return value after either input has been dropped.
Structs that hold references also need lifetime annotations:
struct Config<'a> {
db_host: &'a str,
db_name: &'a str,
}
fn main() {
let host = String::from("localhost");
let name = String::from("mydb");
let config = Config {
db_host: &host,
db_name: &name,
};
println!("Connecting to {} at {}", config.db_name, config.db_host);
}The lifetime 'a on the struct guarantees that a Config cannot outlive the strings it references. If the strings are dropped while a Config still exists, the compiler catches it.
When lifetimes feel overwhelming: consider owning the data instead. Replace &str with String, or &[T] with Vec<T>. Owned data eliminates lifetime annotations entirely and is often the simpler choice, especially for structs that are passed around widely.
Fix 7: Avoid Returning References to Local Data
A function cannot return a reference to a value created inside it, because the value is dropped when the function returns.
Broken — dangling reference:
fn make_greeting(name: &str) -> &str {
let greeting = format!("Hello, {}!", name);
&greeting // error: returns a reference to data owned by the function
}Fixed — return an owned value:
fn make_greeting(name: &str) -> String {
format!("Hello, {}!", name)
}This is not a workaround — it is the correct design. If a function creates new data, it should return ownership of that data. The caller then decides how long to keep it. This pattern is universal in Rust and avoids an entire class of dangling pointer bugs that plague C and C++ codebases.
Fix 8: Iterating and Modifying Collections
One of the most common borrow checker stumbling blocks is trying to modify a collection while iterating over it. The iterator holds an immutable borrow, and pushing or removing elements requires a mutable borrow.
Broken — modifying during iteration:
let mut items = vec![1, 2, 3, 4, 5];
for item in &items {
if *item > 3 {
items.push(*item * 10); // error: cannot borrow as mutable
}
}Fix A — collect indices or values first, then modify:
let mut items = vec![1, 2, 3, 4, 5];
let to_add: Vec<i32> = items.iter().filter(|&&x| x > 3).map(|&x| x * 10).collect();
items.extend(to_add);Fix B — use retain to remove elements by condition:
let mut items = vec![1, 2, 3, 4, 5];
items.retain(|&x| x <= 3);
// items is now [1, 2, 3]Fix C — use drain_filter (nightly) or into_iter().filter().collect() to filter in place:
let items = vec![1, 2, 3, 4, 5];
let items: Vec<i32> = items.into_iter().filter(|&x| x <= 3).collect();The key principle is: separate the reading phase from the writing phase. Read first, collect what needs to change, then apply the changes. This is not a limitation — it prevents iterator invalidation bugs that cause undefined behavior in languages like C++. The same kind of careful ordering applies when resolving git merge conflicts — you identify all conflicts first, then resolve them systematically.
Still Not Working?
Read the full compiler output
Rust has some of the best error messages of any compiler. The output includes the exact locations of conflicting borrows, an explanation of the rule being violated, and often a concrete suggestion:
help: consider borrowing here
|
4 | process(&data);
| +Do not skip the help: and note: lines. They frequently contain the exact fix.
Run cargo clippy for deeper analysis
Clippy catches ownership anti-patterns that compile but are suboptimal:
cargo clippy -- -W clippy::allIt may suggest replacing .clone() with a reference, using .as_ref() on an Option, or restructuring borrows on struct fields.
Split struct borrows
If you have a method on a struct that needs to read one field and write another, the borrow checker may complain because &self and &mut self borrow the entire struct. Extract the work into a free function that takes individual field references:
struct Game {
map: Map,
player: Player,
}
// Instead of a method on Game that borrows all of self:
fn update_player(map: &Map, player: &mut Player) {
player.position = map.find_spawn();
}The compiler can see that &Map and &mut Player are disjoint borrows. This pattern is especially useful in game engines and simulations where state is heavily interconnected.
Check for unnecessary clones and allocations
If your code compiles but you suspect you are cloning too much, look for these patterns:
- Passing
Stringto a function that only reads it — switch to&str. - Cloning a
Vecjust to iterate over it — use&vecor.iter()instead. - Calling
.to_string()or.to_owned()on data that is already in the right format.
Use the entry API for HashMap
Many HashMap borrow conflicts come from checking whether a key exists and then inserting. The entry API handles this without conflicting borrows:
use std::collections::HashMap;
let mut word_counts: HashMap<String, usize> = HashMap::new();
let words = vec!["hello", "world", "hello", "rust"];
for word in &words {
*word_counts.entry(word.to_string()).or_insert(0) += 1;
}Consider whether you need a different data structure
Some borrow checker friction comes from using the wrong data structure. If you find yourself constantly fighting shared mutable access, consider:
SlotMaporArenaallocators — give items stable indices that act like safe pointers.- Entity-component-system (ECS) patterns — separate data by type, not by entity, so borrows rarely overlap.
- Message passing with channels — avoid shared state entirely by sending data between threads.
These approaches align with Rust’s ownership model instead of fighting it. They are widely used in game development, embedded systems, and high-performance networking code.
Ensure environment variables are set correctly
If your borrow checker errors appear only in CI or specific environments, verify that the correct Rust edition and toolchain are being used. An outdated toolchain may lack NLL support or recent borrow checker improvements. Run rustup update to get the latest stable compiler. Environment-specific issues like this are common across languages — see how undefined environment variables cause runtime failures for a broader look at environment-dependent bugs.
Ask the compiler for more detail
Use rustc --explain E0502 (or whatever error code you see) to get a detailed explanation of the rule being enforced. The explanations include examples and are written for humans, not language lawyers. You can also run cargo check for faster feedback than a full cargo build — it skips code generation and only runs the borrow checker and type checker.
Related: For ownership-related errors specific to mutable borrowing conflicts, see Fix: cannot borrow as mutable because it is also borrowed as immutable. If you are dealing with errors in other languages, see Fix: Go declared and not used for Go’s strict unused variable rules, Fix: Java ClassNotFoundException for classpath resolution problems, and Fix: git merge conflict for resolving version control conflicts.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Rust cannot borrow as mutable because it is also borrowed as immutable
How to fix Rust cannot borrow as mutable error caused by aliasing rules, simultaneous references, iterator invalidation, struct method conflicts, and lifetime issues.
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.