The overlap trap
You're porting a data processing script from Python to Rust. You have a vector of measurements. You need to find the maximum value, then divide every element by that maximum to normalize the data. In Python, this is one line: data = [x / max(data) for x in data]. The list is read to find the max, then written to update values. It works.
In Rust, you write the equivalent logic. You call max() to get a reference to the largest element. Then you try to iterate mutably and divide. The compiler rejects the code with E0502 (cannot borrow as mutable because it is also borrowed as immutable). You stare at the error. The logic is sound. The data isn't changing while you read it. Rust disagrees.
The compiler has detected an overlap. An immutable borrow is still active when you attempt to start a mutable borrow. Rust enforces a strict rule: you can have many readers OR one writer, never both. This error is the compiler protecting you from aliasing violations. It guarantees that no reference points to data that might change unexpectedly.
The rule: readers or writer, not both
Rust's memory safety relies on the "aliasing XOR mutability" rule. At any point in time, you can have multiple immutable references to a value, or exactly one mutable reference. You cannot have both.
Think of a shared whiteboard in a meeting. If Alice is pointing at a number and explaining it, Bob cannot walk up and erase that number. If Bob erases it, Alice is pointing at nothing or a new number, and her explanation becomes nonsense. The audience gets confused. Rust prevents this scenario entirely. It ensures Bob waits until Alice puts her hand down before he touches the marker.
When you create an immutable reference (&value), you're telling the compiler "I'm going to read this." The compiler tracks that reference. If you later try to create a mutable reference (&mut value) while the immutable one is still alive, the compiler stops you. The mutable reference would allow changes that could invalidate the immutable reference. The immutable reference might point to stale data, or worse, a dangling pointer if the mutation reallocates memory.
The error E0502 is the compiler's way of saying "I see an immutable borrow that hasn't ended yet. You're trying to get a mutable borrow. This violates the rule."
Minimal example: breaking the overlap
The fix is almost always about timing. You need to ensure the immutable borrow ends before the mutable borrow begins. Rust uses Non-Lexical Lifetimes (NLL), which means the compiler tracks the exact last usage of a borrow, not just the end of the scope block. You can often fix E0502 by reordering operations or extracting values.
fn main() {
let mut numbers = vec![10, 20, 30];
// Immutable borrow starts here.
// The reference lives until the last usage below.
let first = &numbers[0];
// Mutable borrow tries to start here.
// Compiler error E0502: cannot borrow `numbers` as mutable
// because it is also borrowed as immutable.
// The reference `first` is still alive.
numbers.push(40);
println!("First: {}", first);
}
The compiler rejects this because first is used in the println! after the push. The immutable borrow must stay alive until that print happens. The mutable borrow from push overlaps with the immutable borrow.
The solution is to copy the value out of the borrow before mutating. If the type implements Copy, the borrow ends immediately after the copy.
fn main() {
let mut numbers = vec![10, 20, 30];
// Copy the value. i32 implements Copy.
// The immutable borrow ends here because the value is copied out.
let first = numbers[0];
// Mutable borrow starts here.
// No overlap. The compiler allows this.
numbers.push(40);
println!("First: {}, Updated: {:?}", first, numbers);
}
By indexing without the &, you get a copy of the integer. The borrow of numbers ends the moment the copy is made. The mutable borrow for push has no conflict.
Trust the borrow checker. It sees the overlap you missed.
Realistic scenario: updating a struct field
Structs often trigger E0502 when you try to update one field based on another. You borrow the struct immutably to read a field, then try to borrow it mutably to write a different field. The compiler sees a borrow of the whole struct in both cases.
struct User {
name: String,
email: String,
}
fn setup_user(user: &mut User) {
// Immutable borrow of `user` to read `name`.
// The borrow covers the whole struct.
let name = &user.name;
// Mutable borrow of `user` to write `email`.
// Compiler error E0502: cannot borrow `user` as mutable
// because it is also borrowed as immutable.
user.email = format!("{}@example.com", name);
}
The compiler doesn't know that name and email are disjoint fields. It sees a borrow of user and a mutable borrow of user. The overlap is forbidden.
Clone the field you need. This breaks the borrow chain. The clone creates an independent value. The mutable borrow can proceed.
fn setup_user(user: &mut User) {
// Clone the name. This creates a new String.
// The borrow of `user` ends after the clone.
let name = user.name.clone();
// Mutable borrow starts here.
// No overlap. The compiler allows this.
user.email = format!("{}@example.com", name);
}
Cloning is acceptable here because setup_user runs once during initialization. The cost is negligible. In a hot loop, cloning strings is expensive. You would restructure the code to avoid the clone, perhaps by passing the name as a separate argument or computing the email before constructing the user.
Clone the field. The compiler won't argue.
Realistic scenario: graph updates and vectors
Graph algorithms often require reading one node while updating another. If nodes are stored in a vector, you might try to update a node's weight based on a neighbor's weight. This triggers E0502 because you need to read the vector to find the neighbor, then write to the vector to update the current node.
struct Graph {
nodes: Vec<Node>,
}
struct Node {
weight: i32,
neighbor_idx: usize,
}
fn propagate_weight(graph: &mut Graph) {
// Iterate over nodes to update weights.
for i in 0..graph.nodes.len() {
// Immutable borrow to read neighbor index and weight.
let neighbor_idx = graph.nodes[i].neighbor_idx;
let neighbor_weight = graph.nodes[neighbor_idx].weight;
// Mutable borrow to update current node.
// Compiler error E0502: cannot borrow `graph.nodes` as mutable
// because it is also borrowed as immutable.
graph.nodes[i].weight += neighbor_weight;
}
}
The loop holds an immutable borrow of graph.nodes to read neighbor_weight. The update tries to borrow mutably. The overlap occurs inside the loop body.
Extract the values you need before the mutation. Read the neighbor's weight into a local variable. The borrow of the vector ends when the local variable is populated. The mutation can proceed.
fn propagate_weight(graph: &mut Graph) {
for i in 0..graph.nodes.len() {
// Read neighbor index. Borrow ends after this line.
let neighbor_idx = graph.nodes[i].neighbor_idx;
// Read neighbor weight. Borrow ends after this line.
let neighbor_weight = graph.nodes[neighbor_idx].weight;
// Mutable borrow starts here.
// No overlap. The compiler allows this.
graph.nodes[i].weight += neighbor_weight;
}
}
By splitting the reads and the write, you ensure the borrows don't overlap. The compiler sees the immutable borrows end before the mutable borrow begins.
This pattern works because i32 implements Copy. The values are copied out of the vector. If the node data were complex and didn't implement Copy, you would need to clone or restructure the algorithm to process nodes in a way that avoids simultaneous reads and writes.
Extract the values. Keep the borrows short.
Pitfalls: iterators and slices
Iterators often hide borrows. When you call .iter() on a collection, you get an iterator that holds an immutable borrow of the collection. If you try to mutate the collection while the iterator is alive, you get E0502.
fn main() {
let mut data = vec![1, 2, 3];
// Iterator holds an immutable borrow of `data`.
for value in data.iter() {
// Mutable borrow tries to start here.
// Compiler error E0502: cannot borrow `data` as mutable
// because it is also borrowed as immutable.
data.push(*value);
}
}
The iterator must stay alive for the duration of the loop. The mutable borrow overlaps with the iterator's borrow. You cannot mutate a collection while iterating it.
Collect the changes first, then apply them. Or use methods that handle the mutation safely.
fn main() {
let mut data = vec![1, 2, 3];
// Collect new items into a separate vector.
// The iterator borrow ends when `iter()` is consumed.
let new_items: Vec<i32> = data.iter().copied().collect();
// Mutable borrow starts here.
// No overlap. The compiler allows this.
data.extend(new_items);
}
For slices, split_at_mut is the standard tool for non-overlapping mutations. It splits a mutable slice into two mutable slices. The compiler verifies the slices don't overlap. You can mutate both halves independently.
fn main() {
let mut data = [1, 2, 3, 4];
// Split into two mutable slices.
// left covers indices 0..2.
// right covers indices 2..4.
let (left, right) = data.split_at_mut(2);
// Mutate left based on right.
// No overlap. The compiler allows this.
left[0] += right[0];
left[1] += right[1];
println!("{:?}", data);
}
split_at_mut is the pro move for slice manipulation. It gives you exclusive access to disjoint regions. The compiler guarantees safety.
Don't fight the iterator. Collect the changes first, or split the slice.
Decision: when to use what
Use local variables to extract values before mutation when the data is small and cheap to copy. Use split_at_mut when working with slices and you need to read one region while writing to another. Use clone() when the value is complex and you cannot restructure the logic to avoid the overlap. Use RefCell<T> only when the borrow checker blocks a valid design and you accept the runtime overhead of checking rules at execution time. Reach for Cow<T> when you have a function that might or might not need to mutate its input, and you want to avoid cloning in the read-only path.
Pick the tool that matches the data flow. If you're cloning everything, rethink the design.