Interior Mutability
Borrowing Panics
A borrowing panic occurs at runtime when .borrow() or .borrow_mut() is called on a RefCell<T> whose current dynamic borrow state conflicts with the requested access (already mutably borrowed or already borrowed). Because RefCell<T> defers borrow checking from compile time to runtime, overlapping guard lifetimes within the same call stack trigger an immediate panic to prevent undefined behavior.
The Two Panic Conditions
| Call | Active Guard Already Held | Panic Message |
|---|---|---|
cell.borrow() |
RefMut<'_, T> (borrow_flag < 0) |
RefCell already mutably borrowed |
cell.borrow_mut() |
Ref<'_, T> (borrow_flag > 0) |
RefCell already borrowed |
cell.borrow_mut() |
RefMut<'_, T> (borrow_flag < 0) |
RefCell already mutably borrowed |
Common Cause 1: Overlapping Guards in the Same Function
Keeping a Ref<'_, T> binding alive across a subsequent .borrow_mut() call—or calling both inside the same compound expression—panics at runtime:
use std::cell::RefCell;
use std::panic;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// Trap: `data.borrow()` creates a temporary `Ref` that stays alive until the
// end of the statement `;`, while `data.borrow_mut()` runs inside the same statement!
let caught = panic::catch_unwind(panic::AssertUnwindSafe(|| {
data.borrow_mut().push(data.borrow()[0]);
}));
println!("overlapping single-statement borrow panicked: {}", caught.is_err());
// Fix: extract the read value into a separate statement so the temporary `Ref`
// drops before `borrow_mut()` is called:
let first = data.borrow()[0];
data.borrow_mut().push(first);
println!("safe sequential update: {:?}", data.borrow());
}
overlapping single-statement borrow panicked: true
safe sequential update: [1, 2, 3, 1]
Common Cause 2: Re-Entrant Method Calls Across the Call Stack
In event-driven systems or observer patterns, holding a RefMut<'_, T> while invoking a helper method or callback that re-enters the same struct and calls .borrow() or .borrow_mut() panics across stack frames:
Stack Frame 1: `Service::process(&self)`
+-- Acquires `let mut state = self.state.borrow_mut();` (borrow_flag = -1)
+-- Calls `self.log_status()` while `state` guard is still in scope!
|
v
Stack Frame 2: `Service::log_status(&self)`
+-- Calls `self.state.borrow()` ---> PANIC: RefCell already mutably borrowed!
use std::cell::RefCell;
struct Service {
items: RefCell<Vec<u32>>,
}
impl Service {
fn log_count(&self) {
println!("current item count: {}", self.items.borrow().len());
}
fn add_and_log_safely(&self, item: u32) {
{
let mut guard = self.items.borrow_mut();
guard.push(item);
} // `guard` drops here before calling `self.log_count()`!
self.log_count();
}
}
fn main() {
let svc = Service {
items: RefCell::new(vec![10]),
};
svc.add_and_log_safely(20);
}
current item count: 2
Strategies to Eliminate Borrowing Panics
- Scope every
RefandRefMuttightly: Wrap mutations in{ ... }blocks or calldrop(guard)before calling any other function or method. - Never invoke unknown callbacks while holding a
ReforRefMut: Clone or take the callback list out of theRefCellfirst, drop the guard, and then invoke the callbacks. - Prefer
Cell<T>when possible: BecauseCell<T>never hands out inner references,.get(),.set(),.take(), and.replace()can never panic even under re-entrant calls. - Use
.try_borrow()and.try_borrow_mut()when contention is a valid runtime state that should be handled viaResult.