The borrow checker fight
You're writing a function that processes a list of user inputs. You want to filter out empty strings, then update a counter for each valid entry. You write let valid = inputs.iter().filter(...); inputs.push(...);. The compiler screams. You have an immutable borrow from iter and you're trying to mutate with push. This happens constantly. The borrow checker isn't trying to annoy you. It's preventing data races and use-after-free bugs at compile time. The patterns to satisfy it are simple once you see the shape of the problem.
The rules and the whiteboard
Rust enforces two rules for references. You can have either one mutable reference or any number of immutable references. You cannot have both at the same time. This is aliasing XOR mutability.
Think of a whiteboard in a meeting room. Anyone can look at the whiteboard. If you want to write on it, you need to be the only one looking. If someone is writing, no one else can look. The borrow checker is the bouncer at the door. It checks who has copies of the data and who has the pen. If the bouncer sees a reader and a writer at the same time, it stops the program before it runs.
This rule guarantees that no two parts of your code can corrupt the same data simultaneously. It also guarantees that a reference never points to garbage memory. If the owner drops the data, all references must be gone. The compiler proves this before your code executes.
Minimal example: NLL saves you
Rust uses Non-Lexical Lifetimes. The compiler tracks the last use of a borrow, not just the scope where it was created. This means borrows end as soon as they are no longer needed.
fn main() {
let mut data = String::from("hello");
// Create an immutable borrow.
let r1 = &data;
// Another immutable borrow is allowed.
let r2 = &data;
// Use both to prove they are alive.
println!("{}, {}", r1, r2);
// r1 and r2 are no longer used.
// The immutable borrows end here automatically.
// A mutable borrow is now allowed.
let r3 = &mut data;
r3.push_str(" world");
println!("{}", r3);
}
The compiler sees that r1 and r2 are used in the println. After that line, they are dead. The mutable borrow r3 can start. You don't need manual drop calls. NLL handles the cleanup.
Trust the borrow checker on lifetimes. It tracks usage precisely.
Pattern: Reorder your operations
The most common fix is reordering. Often you borrow data, use it briefly, and then try to mutate the owner. If you move the mutation before the borrow, or use the borrow before the mutation, the conflict vanishes.
struct Config {
settings: Vec<String>,
last_updated: u64,
}
impl Config {
fn get_setting(&self, key: &str) -> Option<&str> {
self.settings.iter().find(|s| s.starts_with(key)).map(|s| s.as_str())
}
fn update_timestamp(&mut self) {
self.last_updated = 12345;
}
}
fn main() {
let mut config = Config {
settings: vec!["debug=true".to_string()],
last_updated: 0,
};
// This fails: E0502 cannot borrow as mutable because it is also borrowed as immutable.
// let setting = config.get_setting("debug");
// config.update_timestamp();
// println!("{}", setting.unwrap());
// Fix: Use the borrow before mutating.
let setting = config.get_setting("debug");
println!("{}", setting.unwrap());
// The borrow from get_setting ends here.
config.update_timestamp();
}
The compiler rejects the broken code with E0502 (cannot borrow as mutable because it is also borrowed as immutable). The fix is trivial. Use the data, then mutate.
Reorder before you clone. It costs nothing.
Pattern: Clone small data
If reordering doesn't work, clone the data. Cloning creates an independent copy. The copy has its own owner. You can mutate the original and the copy without conflict.
Cloning is cheap for small data. A String with a few characters, a small Vec, or a simple struct fits in a cache line. The cost of copying is negligible.
fn main() {
let mut names = vec!["Alice".to_string(), "Bob".to_string()];
// Clone the vector to avoid borrowing the original.
let snapshot = names.clone();
// Mutate the original.
names.push("Charlie".to_string());
// Use the snapshot safely.
for name in &snapshot {
println!("Original list had: {}", name);
}
}
Convention aside: When cloning, be explicit about the cost. If you clone a large structure, add a comment explaining why. let snapshot = names.clone(); // Clone is cheap: max 10 elements. This helps reviewers understand the trade-off.
Clone early if the data is small. It removes complexity for zero performance penalty.
Pattern: Split borrows
Sometimes you need to mutate two parts of the same data structure. Rust allows this if the parts don't overlap. You can split a slice or a vector into two mutable borrows.
fn main() {
let mut data = vec![1, 2, 3, 4, 5];
// Split the vector into two non-overlapping mutable slices.
let (left, right) = data.split_at_mut(2);
// left is [1, 2], right is [3, 4, 5].
// You can mutate both independently.
left[0] += 10;
right[0] += 100;
println!("{:?}", data); // [11, 2, 103, 4, 5]
}
The compiler knows that left and right cover disjoint indices. It allows both mutable borrows. This pattern is essential for algorithms like quicksort or swapping elements.
Use split_at_mut when you need to mutate disjoint parts of a collection. It's safe and efficient.
Pattern: Cow for conditional cloning
Cow stands for Clone on Write. It wraps data that can be either borrowed or owned. If you only read the data, Cow holds a reference. If you modify it, Cow clones the data automatically and switches to owned mode.
This is useful when you have a function that might modify data, but often doesn't. You avoid cloning in the read-only path.
use std::borrow::Cow;
fn process_input(input: &str) -> Cow<str> {
if input.contains("secret") {
// Modification needed: clone and redact.
Cow::Owned(input.replace("secret", "REDACTED"))
} else {
// No modification: return a borrowed reference.
Cow::Borrowed(input)
}
}
fn main() {
let text = "Hello world";
// No allocation happens here.
let result = process_input(text);
println!("{}", result);
let sensitive = "This is a secret";
// Allocation happens here only when needed.
let result2 = process_input(sensitive);
println!("{}", result2);
}
Cow saves allocations when mutations are rare. It adds a small runtime check to determine if the data is borrowed or owned.
Use Cow when the clone is expensive and the mutation is rare. It optimizes the common case.
Pattern: Interior mutability with RefCell
Sometimes you cannot reorder, clone, or split. You need to mutate data through an immutable reference. This is interior mutability. RefCell provides this by moving the borrow check to runtime.
RefCell tracks borrows at runtime. It panics if you violate the rules. This is a trade-off. You gain flexibility but lose compile-time guarantees.
use std::cell::RefCell;
struct Counter {
value: RefCell<u32>,
}
impl Counter {
fn increment(&self) {
// Borrow mutably through an immutable reference.
let mut val = self.value.borrow_mut();
*val += 1;
}
fn get(&self) -> u32 {
let val = self.value.borrow();
*val
}
}
fn main() {
let counter = Counter { value: RefCell::new(0) };
// Multiple immutable references can exist.
let r1 = &counter;
let r2 = &counter;
r1.increment();
r2.increment();
println!("{}", counter.get()); // 2
}
RefCell is the escape hatch. Use it when the borrow checker blocks a valid design and you've exhausted other options. The runtime check costs cycles. It also risks panics if you hold a borrow across a function call that tries to borrow again.
Don't reach for RefCell until you've tried reordering. Runtime checks cost cycles.
Pitfalls and compiler errors
The borrow checker produces specific errors. Understanding them helps you fix code faster.
E0502 (cannot borrow as mutable because it is also borrowed as immutable) means you have an active immutable reference and you're trying to mutate. Reorder, clone, or split.
E0382 (use of moved value) means you moved ownership and tried to use the original. Clone the value or pass a reference.
E0507 (cannot move out of borrowed content) means you're trying to move data out of a reference. You can only move out of owned data. Clone the data or use std::mem::take if the type supports it.
E0716 (temporary value dropped while borrowed) means you created a temporary and tried to borrow it. The temporary dies at the end of the statement. Store the temporary in a variable first.
The compiler rejects this with E0507 when you try to extract a field from a reference. You cannot move data out of a borrow. Clone the field or restructure the code.
Treat the error code as a diagnosis. It tells you exactly which rule you broke.
Decision: When to use what
Use references when you need to read data without copying and the owner lives long enough. References are zero-cost and safe.
Use clone when you need an independent copy that can outlive the original or be mutated separately. Clone small data freely. Clone large data only when necessary.
Use Cow when you want to avoid cloning unless you actually modify the data. It optimizes the read-only path.
Use RefCell when you need interior mutability and single-threaded access. It moves checks to runtime. Use it sparingly.
Use Rc when you have multiple owners and single-threaded access. It shares ownership with reference counting.
Use Arc when you have multiple owners and multi-threaded access. It is atomic and thread-safe.
Convention aside: When cloning an Rc, write Rc::clone(&rc) instead of rc.clone(). Both compile, but the explicit form signals that this is a cheap counter bump, not a deep copy. The community prefers this clarity.
Use the simplest tool that solves the problem. References beat clones. Clones beat Cow. Cow beats RefCell.