Rust Patterns That Matter #7: Split Borrows
Post 7 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.
Previous: #6: Rc<RefCell<T>> | Next: #8: Index-Based Design
You have a struct with two fields. A method needs to read one and write the other. The compiler blocks it - it sees a borrow of the whole struct, not individual fields. The fix is usually a small structural change, not a fight.
The motivation
A renderer holds both a configuration and a buffer. The render method
reads the configuration to decide what to draw, then writes to the buffer.
struct Renderer {
config: Config,
buffer: Vec<u8>,
}
impl Renderer {
fn render(&mut self) {
let width = self.get_width(); // borrows &self
self.buffer.resize(width, 0); // borrows &mut self — ERROR
}
fn get_width(&self) -> usize {
self.config.width
}
}
The compiler:
error[E0502]: cannot borrow `self.buffer` as mutable because
it is also borrowed as immutable
get_width takes &self, which borrows the entire
struct. While that borrow is alive (because width could theoretically
be a reference into self), the compiler won't let you mutably borrow
self.buffer.
Why the compiler does this
Method calls taking &self or &mut self borrow
the whole struct. The compiler doesn't look inside the method to see which
fields it actually touches. From its perspective, get_width(&self)
might be reading self.buffer, and if it is, then the simultaneous
&mut self.buffer would be unsound.
Direct field access is different. If you write self.config.width
instead of calling a method, the compiler can see that the borrow is only on
self.config, and self.buffer is disjoint. The problem
is specifically about method calls that borrow the whole struct.
Pattern 1: Direct field access
The simplest fix - inline the field access instead of calling a method:
fn render(&mut self) {
let width = self.config.width; // borrows self.config only
self.buffer.resize(width, 0); // borrows self.buffer — disjoint, OK
}
The compiler knows self.config and self.buffer are
different fields. No conflict. This works because the compiler does do field-level
borrow tracking for direct access - it's method calls that erase field
information.
Pattern 2: Destructuring
When you need to work with multiple fields across several operations, destructure the struct to give each field its own binding:
fn render(&mut self) {
let Renderer { config, buffer } = self;
// config: &Config (borrowed from self)
// buffer: &mut Vec<u8> (borrowed from self)
// These are disjoint borrows — compiler is happy
let width = config.width;
buffer.resize(width, 0);
}
Destructuring splits self into independent borrows. The compiler can
see that config and buffer don't overlap.
Pattern 3: Helper functions on fields
If the logic is complex enough to warrant methods, make them take the individual
fields rather than &self:
impl Renderer {
fn render(&mut self) {
Renderer::fill_buffer(&self.config, &mut self.buffer);
}
fn fill_buffer(config: &Config, buffer: &mut Vec<u8>) {
buffer.resize(config.width, 0);
}
}
The function signature explicitly takes &Config and
&mut Vec<u8>. The compiler can see these are disjoint
borrows of self.
Pattern 4: Temporary variables
If you only need to read a value from one field, copy or clone it before touching the other field:
fn render(&mut self) {
let width = self.get_width(); // borrows and releases &self
// width is an owned usize — no borrow alive
self.buffer.resize(width, 0); // fine — no conflicting borrow
}
Wait - this is the original code that failed. Why does it work? Actually,
it depends on the return type. If get_width returns usize
(copied, no borrow), the shared borrow of self is released before
self.buffer is mutably borrowed. If it returned &usize,
the borrow would persist. The key insight: the compiler extends borrows only as long
as the returned reference lives.
The key insight
The compiler is smarter than it might seem. It tracks borrows at the field level for
direct access. The problem comes specifically from method calls, which borrow the
whole struct via &self/&mut self. The fixes all
share one strategy: give the compiler enough information to see that the borrows
are disjoint.
When to use it
Anytime you get "cannot borrow X as mutable because it is also borrowed as immutable" and you know the borrows are on different fields. The fix is usually one of:
- Inline the field access instead of calling a method
- Destructure the struct
- Use free functions or associated functions that take fields separately
- Copy/clone the value to end the borrow before mutating
If none of these work and you genuinely have aliased mutable access, the borrow checker is right to reject it. But most "split borrow" situations are false positives that one of these patterns resolves.
See it in practice: Building a Chat Server #2: Rooms and Users uses this pattern for reading rooms while writing to users.
Telex