The "Two Hands on the Wheel" Error
You are writing a game loop. The GameState struct holds the player position, the enemy list, and the camera. You write a method to update everything: move the player, update enemies, then pan the camera to follow the player. You call self.update_player(), then self.update_enemies(), then self.update_camera(). The code looks clean. You run cargo check and the compiler rejects you with a borrow error. It claims you are borrowing self as mutable more than once.
The error often appears in a slightly different shape. You try to assign a value back to a field using a method that mutates the struct: self.cache = self.recompute_cache(). Or you hold a reference to a field and then try to mutate the struct: let log = &mut self.log; self.score += 1;. In every case, Rust is drawing a hard line. You cannot hold two mutable handles to the same data at the same time.
Rust enforces exclusive access to mutable data. This rule prevents data races in concurrent code and ensures that references never point to garbage memory. When you see a borrow error involving self, the compiler has found a path where a mutable borrow is still active when you attempt to start another one. The overlap might be obvious, or it might hide inside argument evaluation order. The fix is almost always about making the boundaries of your borrows explicit.
The compiler isn't being difficult. It's protecting you from tearing your own data apart.
Why Rust says no
Imagine a physical key to a server room. Only one person can hold the key at a time. If Alice holds the key, Bob cannot enter. If Alice hands the key to Bob, Alice must leave the room first. Rust treats &mut self like that key. When a method takes &mut self, it takes the key. No other code can touch the struct until the method returns and gives the key back.
The danger arises when you try to use the key while you're already holding it. If you have a reference to self.x and then call a method that takes &mut self, that method might change self.x, invalidate your reference, or move data around. Rust refuses to let that happen. It requires you to drop the reference before you take the mutable borrow.
This rule also applies to method chains and argument evaluation. Rust does not guarantee the order in which function arguments are evaluated. If you write self.method_a(self.method_b()), the compiler must assume that method_a could borrow self before method_b runs, or that method_b could run while method_a holds a borrow. Since both methods likely need &mut self, the compiler rejects the call to avoid a potential conflict.
Mutability is a privilege, not a right. You have to prove you're the only one holding the pen.
The minimal fix: Scoping and temps
The most common fix is to break the overlapping borrows into distinct steps. You can do this with explicit scopes or temporary variables.
struct Buffer {
data: Vec<i32>,
checksum: i32,
}
impl Buffer {
/// Computes the checksum by iterating over data.
fn compute_checksum(&mut self) -> i32 {
let sum: i32 = self.data.iter().sum();
sum
}
/// Updates the stored checksum.
fn update_checksum(&mut self, value: i32) {
self.checksum = value;
}
/// Refreshes the checksum field.
fn refresh(&mut self) {
// ERROR: self.compute_checksum() borrows self mutably.
// self.update_checksum() also borrows self mutably.
// The compiler cannot guarantee these borrows don't overlap.
// self.update_checksum(self.compute_checksum());
// FIX: Compute first, then update.
let new_checksum = self.compute_checksum();
self.update_checksum(new_checksum);
}
}
The temporary variable new_checksum holds the result of the first borrow. Once compute_checksum returns, the mutable borrow of self ends. The compiler sees that self is free before update_checksum starts. The key is returned, then taken again.
When you need to mutate a field and then use the whole struct, a scope block works well.
struct Logger {
messages: Vec<String>,
active: bool,
}
impl Logger {
/// Adds a message to the log.
fn log(&mut self, msg: &str) {
self.messages.push(msg.to_string());
}
/// Processes the log and deactivates the logger.
fn process_and_stop(&mut self) {
// ERROR: &mut self.messages borrows part of self.
// self.active = false borrows self mutably.
// Overlap detected.
// let log_ref = &mut self.messages;
// log_ref.push("Stopping".to_string());
// self.active = false;
// FIX: Scope the field borrow.
{
let log_ref = &mut self.messages;
log_ref.push("Stopping".to_string());
}
// log_ref is dropped here. The borrow ends.
self.active = false;
}
}
The block {} creates a scope. The reference log_ref lives only inside the block. When the block ends, the borrow ends. The compiler can now see that self.active is safe to mutate.
Break the chain. Compute first, assign second.
The argument evaluation trap
The argument evaluation order is undefined in Rust. This is a deliberate design choice that allows the compiler to optimize code generation. It also creates a hidden trap for &mut self methods.
Consider this code:
struct Counter {
value: i32,
}
impl Counter {
/// Increments the counter.
fn increment(&mut self) {
self.value += 1;
}
/// Returns the current value.
fn get(&self) -> i32 {
self.value
}
/// Tries to increment by the current value.
fn bad_increment(&mut self) {
// ERROR: self.increment() takes &mut self.
// self.get() takes &self.
// The compiler must assume the borrows could overlap.
// self.increment(self.get());
}
}
Even though increment and get access different aspects of the data, the compiler rejects the call. It cannot prove that get runs before increment acquires the mutable borrow. If increment ran first, it would mutate self while get is trying to read it. The compiler errs on the side of safety.
The fix is the same: separate the calls.
impl Counter {
/// Increments by the current value safely.
fn good_increment(&mut self) {
let current = self.get();
self.increment();
// If you needed to add current, you'd do:
// self.value += current;
}
}
This pattern appears constantly in real code. You will see it when calling self.sort() inside a comparison function, or when passing self.next() to self.consume(). The compiler error will often cite E0502 (cannot borrow as mutable because it is also borrowed as immutable) or E0499 (conflicting borrows). The solution is always to materialize the values before the mutation.
Read the error code. E0502 means you're holding a read lock while trying to write. Drop the read lock.
Realistic scenario: The parser state
Parsers are a common source of borrow errors. A parser often holds a buffer of input, a current position, and a stack of tokens. Methods like peek() and advance() mutate the position, while parse_expression() might call both.
struct Parser {
input: Vec<char>,
pos: usize,
}
impl Parser {
/// Returns the character at the current position without advancing.
fn peek(&self) -> Option<char> {
self.input.get(self.pos).copied()
}
/// Advances the position by one.
fn advance(&mut self) {
if self.pos < self.input.len() {
self.pos += 1;
}
}
/// Tries to consume a specific character.
fn try_consume(&mut self, target: char) -> bool {
// ERROR: self.peek() borrows self immutably.
// self.advance() borrows self mutably.
// The call self.advance() happens inside the if, but the borrow
// from peek might extend depending on evaluation.
// if self.peek() == Some(target) {
// self.advance();
// true
// } else {
// false
// }
// FIX: Peek first, store result, then advance.
if self.peek() == Some(target) {
self.advance();
true
} else {
false
}
}
}
In this case, modern Rust's Non-Lexical Lifetimes (NLL) often allow the if self.peek() == Some(target) pattern because the compiler can see that the result of peek is used immediately and the borrow ends before advance runs. However, if you capture the result in a more complex way, or if peek returns a reference, the error returns.
The convention in parser code is to separate inspection from mutation. Store the peeked value in a local variable. This makes the intent clear to the compiler and to human readers.
Scope is your friend. Drop the reference before you touch the rest of the struct.
Slices and split_at_mut
Slices present a unique challenge. You often need to mutate two different parts of a slice at the same time. For example, swapping elements, or shifting data.
fn shift_left(slice: &mut [i32]) {
// ERROR: slice[0] borrows mutably.
// slice[1..] borrows mutably.
// The compiler sees overlapping ranges.
// slice[0] = slice[1];
}
Simple indexing often works due to split borrows, but loops or complex indexing trigger errors. The solution is split_at_mut. This method splits a slice into two mutable slices at a given index. The compiler knows the slices are disjoint, so it allows simultaneous mutation.
fn shift_left(slice: &mut [i32]) {
if slice.len() < 2 {
return;
}
// Split into left (index 0) and right (index 1..).
let (left, right) = slice.split_at_mut(1);
// Safe: left and right are disjoint.
left[0] = right[0];
}
This pattern is essential for algorithms like quicksort, where you need to swap elements across a pivot, or for streaming data where you write to a buffer while reading from a different region.
The community convention is to name the split parts clearly. let (header, body) = buf.split_at_mut(4); reads better than let (a, b) = ....
When you need two mutable pointers into the same data, split the data first.
Pitfalls and compiler errors
You will encounter a few specific error codes when fighting this problem.
E0502 is the classic "cannot borrow as mutable because it is also borrowed as immutable." This happens when you hold an immutable reference and try to mutate. The fix is to drop the immutable reference or use a temporary variable.
E0499 indicates conflicting borrows. This often appears when you try to borrow self mutably twice in the same expression. The fix is to separate the borrows into distinct statements.
E0507 can appear if you try to move a field out of self while self is borrowed. This is less common but related. The fix is to copy the field or use std::mem::take.
A subtle pitfall is method chaining. self.method_a().method_b() works if method_a returns &mut Self. If method_a returns a value and method_b is called on self, the chain breaks. Ensure your methods return the right type for chaining.
Another pitfall is holding a reference across a loop. If you borrow a field at the start of a loop and mutate self inside the loop, the borrow persists. Move the borrow inside the loop or use a temporary.
Convention aside: let temp = self.field.clone(); is a common pattern to break borrows when you need to read a field and mutate the struct. The clone creates an independent copy, releasing the borrow on self.
Read the error code. E0502 means you're holding a read lock while trying to write. Drop the read lock.
When scopes aren't enough: Interior mutability
Sometimes the borrow patterns are too dynamic for the compiler to verify. You might need to mutate data through a shared reference, or you might have a data structure where ownership is unclear. In these cases, Rust provides interior mutability types like RefCell<T>.
RefCell<T> enforces the borrow rules at runtime instead of compile time. You can have multiple &RefCell<T> references, and each can mutate the inner data. If you violate the rules, the program panics.
use std::cell::RefCell;
struct SharedState {
data: RefCell<Vec<i32>>,
}
impl SharedState {
/// Pushes a value, checking borrows at runtime.
fn push(&self, value: i32) {
let mut guard = self.data.borrow_mut();
guard.push(value);
}
}
Use RefCell sparingly. It adds runtime overhead and risks panics. It is a workaround, not a first-class solution. If you find yourself reaching for RefCell to fix a borrow error, consider refactoring your API. Passing specific fields or restructuring the data often eliminates the need for interior mutability.
Runtime checks are a tax. Pay them only when compile-time checks can't see the truth.
Decision: When to use what
Use explicit scopes when you need to mutate self in steps and the compiler can't see that the first borrow ends. Wrap the first borrow in {} to force it to drop.
Use temporary variables when you need to read from self and write back to self in the same expression. Store the read value in a local variable before mutating.
Use split_at_mut when you need simultaneous mutable access to disjoint parts of a slice or array. This is the safe way to implement swaps and shifts.
Use RefCell<T> when you need interior mutability and the borrow patterns are too dynamic for the compiler to verify at compile time. Accept the runtime cost and panic risk.
Refactor your API when you find yourself fighting the borrow checker repeatedly. Passing specific fields or splitting methods often beats passing &mut self everywhere.
Refactoring beats workarounds. If you need RefCell to fix a borrow error, your API design might be fighting Rust.