Working With the Borrow Checker
The borrow checker is not your enemy. It is a static analysis tool that prevents a class of bugs that compilers in other languages let through: dangling references, use-after-free, data races, and iterator invalidation. When it rejects your code, it is telling you that the code has a structural problem, even if that problem would never surface at runtime in your specific test case.
This tutorial covers the most common scenarios where the borrow checker pushes back, what it is actually protecting you from, and the idiomatic patterns to write clearer code that it accepts.
Scenario 1 - Modifying a field while iterating over another
The Problem
You have a struct with two fields. You want to iterate over one field while updating another. This seems fine. They are independent fields, but the compiler still rejects it.
struct Processor {
items: Vec<String>,
processed: usize,
}
impl Processor {
fn run(&mut self) {
for item in &self.items { // immutable borrow of `self`
println!("{}", item);
self.processed += 1; // ERROR: mutable borrow of `self`
}
}
}
error[E0506]: cannot use `self.processed` because it is borrowed
The compiler sees a borrow of self (via &self.items) and a mutable access to self (via self.processed) active at the same time. It cannot prove they do not overlap.
The Pattern
Option A: Pre-compute the count before the loop.
If all you need is the count, compute it before borrowing:
fn run(&mut self) {
let count = self.items.len();
for item in &self.items {
println!("{}", item);
}
self.processed += count;
}
Option B: Borrow only the field you need, not the whole struct.
Instead of iterating through self, move the field reference outside:
fn run(&mut self) {
let items = &self.items; // borrow only items
for item in items {
println!("{}", item);
self.processed += 1; // now self.processed is a distinct field — OK
}
}
This works because the compiler can track borrows at the field level, not just the struct level. By explicitly borrowing self.items into a local variable, the compiler sees that self.processed is a separate memory location.
Tip: If you find yourself fighting this pattern repeatedly, it is often a sign that the struct is doing too much. A struct that mixes "input data" and "output state" can often be split into two, with the consumer holding a reference to one and owning the other.
Scenario 2 - Returning a reference to something you created
The Problem
You create a value inside a function and try to return a reference to it.
fn get_greeting() -> &str {
let msg = String::from("hello");
&msg // ERROR: `msg` does not live long enough
}
The string msg is dropped at the end of get_greeting. The reference would point to freed memory. The borrow checker catches this before it becomes a problem.
The Pattern
Return owned data when the function creates the value.
fn get_greeting() -> String {
String::from("hello")
}
Return a reference only when the data comes from the caller.
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
Here, the returned reference borrows from s, which the caller owns and keeps alive. The lifetime is implicit but correct: the returned &str cannot outlive s.
Use Cow<str> when you sometimes return a reference and sometimes return owned data.
use std::borrow::Cow;
fn sanitize(input: &str) -> Cow<str> {
if input.contains('<') {
Cow::Owned(input.replace('<', "<"))
} else {
Cow::Borrowed(input) // no allocation if no sanitization needed
}
}
Cow (Clone On Write) avoids allocating when the input does not need modification and still returns a usable value in both cases. The caller treats both variants identically via Deref.
Scenario 3 - Holding a reference while also modifying the collection
The Problem
You search for an element in a collection, get a reference to it, and then want to modify the collection based on what you found.
fn promote(employees: &mut Vec<String>, name: &str) {
let found = employees.iter().find(|e| e.as_str() == name);
if found.is_some() {
employees.push(String::from("promoted: ") + name); // ERROR
}
}
error[E0502]: cannot borrow `employees` as mutable because it is also borrowed as immutable
found borrows from employees. As long as found is alive, employees cannot be mutated.
The Pattern
Do not hold the reference. Hold the result you need from it.
fn promote(employees: &mut Vec<String>, name: &str) {
let found = employees.iter().any(|e| e.as_str() == name);
if found {
employees.push(format!("promoted: {}", name));
}
}
.any() returns a bool instead of a reference. The borrow ends before the mutation begins.
When you need an index, not a reference:
fn promote(employees: &mut Vec<String>, name: &str) {
let index = employees.iter().position(|e| e.as_str() == name);
if let Some(i) = index {
employees[i] = format!("promoted: {}", &employees[i]);
}
}
Storing an index instead of a reference sidesteps the borrow entirely. The index is just a usize. It does not borrow anything.
Tip: Borrow checker errors that say "borrowed here" and "used here later" are often resolved by consuming or copying the result of a query before going back to mutate the source. Ask yourself: "Do I actually need to hold onto this reference, or do I just need a value derived from it?"
Scenario 4 - Mutably borrowing two parts of the same Vec
The Problem
You want to swap or compare elements at two different positions in a slice, but taking two &mut references from the same slice is not allowed.
fn swap_by_value(v: &mut Vec<i32>, a: usize, b: usize) {
let x = &mut v[a];
let y = &mut v[b]; // ERROR: cannot borrow `*v` as mutable more than once
std::mem::swap(x, y);
}
The Pattern
Use the built-in .swap() method, which the standard library implements safely with unsafe internals.
fn swap_by_value(v: &mut Vec<i32>, a: usize, b: usize) {
v.swap(a, b);
}
For more complex cases, use split_at_mut to divide the slice into two non-overlapping mutable parts.
fn add_left_to_right(v: &mut [i32], mid: usize) {
let (left, right) = v.split_at_mut(mid);
right[0] += left[left.len() - 1];
}
split_at_mut gives you two &mut slices that the compiler knows do not overlap. It is purpose-built for exactly this situation.
Scenario 5 - Using clone strategically
Clone is not inherently a sign of bad code. Unnecessary clones in tight loops are a performance concern, but cloning at a system boundary is often the right call.
When to clone
At system boundaries. If a function receives owned data and hands it to a subsystem that takes ownership, cloning once at the entry point is cleaner than threading lifetimes through multiple layers.
fn process(config: Config) {
let audit_copy = config.clone(); // one clone, then both paths are clean
run_main(config);
audit_log(audit_copy);
}
When the alternative is complex lifetime wrangling. If a lifetime annotation would require propagating 'a through three struct definitions, a single .clone() may be the more maintainable choice.
When not to clone
Inside a loop over large data. Each .clone() in a hot loop is an allocation. Reach for references, indexes, or reorganizing ownership instead.
When you only need to read, not own. Accept &T instead of T if the function does not need to own the value.
// Prefer this
fn display(name: &str) { println!("{}", name); }
// Over this
fn display(name: String) { println!("{}", name); }
The &str version works with both String and &str callers without requiring a clone.
Key Takeaways
- Borrow checker errors are structural signals. Before fighting back with
clone()orRefCell, read what the error is protecting. - Borrow struct fields directly (not
self) when you need to split an immutable and mutable access. - Return owned data from functions that create values. Return references only when borrowing from the caller.
- Replace references-as-results with
bool, index, or a derived value to end a borrow before mutation begins. - Use
.swap()andsplit_at_mutfor mutable access to two parts of the same slice. - Clone at boundaries, not inside loops.