Fix: Rust Borrow Checker Errors – Cannot Borrow as Mutable, Value Moved, and Lifetimes
Part of: Go, Rust & Systems Errors
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.
Diagnostic Timeline
When a borrow checker error stops your build, it is tempting to slap .clone() on the value and move on. Sometimes that is the right call. More often it hides the underlying design issue and you keep paying for it — both in allocations and in code that becomes harder to refactor later. Here is the order an experienced Rust developer actually works through.
First reaction: clone it. You reach for .clone() because the diff is one line and the compiler stops complaining. Reject this as the default. Cloning a String is a heap allocation. Cloning a Vec<T> is a deep copy of every element. If you cloned because the function only reads the value, you wasted memory. Worse, the next person to read your code cannot tell whether you cloned intentionally or to silence the borrow checker.
Second reaction: read the spans, not just the message. Rust 2021 and later editions ship dramatically better diagnostics than the early 1.x days. The compiler underlines the first borrow, the conflicting second borrow, and the later use that keeps the first one alive. Before doing anything, identify which of the three canonical patterns you are looking at:
- Use after move (
E0382). Ownership was transferred, then the original variable was read. - Mutable while immutable borrowed (
E0502,E0499). A&mutwas taken while a&(or another&mut) is still alive. - Self-referential or returning local reference (
E0515,E0106). A struct or function tries to hold a reference into something it owns or just created.
Each pattern has a different fix. Conflating them sends you down the wrong path.
Third reaction: blame NLL. If the code looks like it should work — borrow ends before next use — you may be on a pre-2018 edition. Check Cargo.toml. With edition = "2015", the compiler uses lexical borrow scopes and many borrows that should be valid are rejected. Non-Lexical Lifetimes (NLL) shipped in the 2018 edition and were extended in later releases. Upgrading the edition often eliminates whole classes of false positives.
Actual root cause. For use-after-move errors, ask: does the function genuinely need to own the value, or could it borrow? Switch to &T or &mut T and the move disappears. For mutable-while-borrowed, reorder so the read borrow ends before the write begins, or split the data so the borrows touch disjoint fields. For self-referential structs (a struct holding a reference into another of its own fields), the borrow checker is correct — restructure to use indices, Rc<RefCell>, or a separate owning container. Reach for Rc<RefCell<T>> or Arc<Mutex<T>> only after confirming a single-owner design genuinely cannot express what you need.
Run cargo check (not cargo build) for fast feedback while iterating. It skips code generation and only runs type and borrow checks.
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.
Check whether the error is actually a trait bound or lifetime mismatch in disguise
Some borrow-checker-looking errors are really lifetime mismatches in trait bounds or generic constraints. A function that takes impl Fn(&str) -> &str requires you to specify the lifetime relationship explicitly when the returned reference depends on a captured variable. If your error mentions trait objects (dyn Trait) and lifetimes in the same paragraph, you are dealing with the 'static bound or higher-ranked trait bounds rather than a simple borrow conflict. Boxing the value (Box<dyn Trait>) or owning the input (taking String instead of &str) is often the cleanest fix.
Look for hidden borrows from temporary values
Method chains like vec.iter().filter(...).collect() create temporary iterators that hold borrows on the source. If you try to mutate vec while a chain is still being constructed on another line, you get a borrow conflict that is hard to spot because the temporary has no name. Break the chain into a let binding so the temporary’s scope is explicit, then look at where each borrow actually ends.
Confirm the toolchain matches your CI
Borrow checker improvements ship in nearly every Rust release. Code that compiles locally on stable 1.78 may fail on the older toolchain pinned in your CI image. Check rust-toolchain.toml or the CI configuration and align them. The same logic applies the other way — if CI just bumped to a new stable release and your local toolchain is older, install the matching version with rustup install <version>. Similar version-skew issues bite npm dependency resolution and Go module resolution when the toolchain on one machine differs from another.
Static analysis beyond the borrow checker
Once your code compiles, run Java OutOfMemoryError-style heap inspection for parallel context — but for Rust specifically, cargo bloat and cargo expand help diagnose whether your design is exploding allocations because of clones added under pressure. Pair this with profiling so you can quantify whether a clone you reluctantly added is actually a hot path. Borrow-checker issues sometimes cluster around the same files that produce Java ClassCastException-style runtime mismatches in other languages — both come from unclear ownership of identity.
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: Maturin Not Working — develop Errors, ABI3 Wheels, manylinux, and macOS Universal Builds
How to fix Maturin errors — maturin develop fails outside venv, abi3 forward compatibility, manylinux wheel auditing, macOS universal2 cross-compile, pyproject.toml vs Cargo.toml conflicts, and PyO3 feature flags.
Fix: PyO3 Not Working — Bound API Migration, GIL Acquisition, Error Conversion, and NumPy Interop
How to fix PyO3 errors — &PyAny vs Bound<PyAny> migration, GIL acquire/release patterns, returning Rust errors as Python exceptions, numpy ndarray zero-copy, pyclass frozen, and async tokio integration.
Fix: Tauri 2 Not Working — Capabilities, Plugin Permissions, Mobile Build, and v1 Migration
How to fix Tauri 2 errors — invoke command not allowed by capabilities, plugin permission missing, tauri.conf.json schema, mobile init/build failures, updater migration, and v1 allowlist conversion.
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.