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

  1. Scope every Ref and RefMut tightly: Wrap mutations in { ... } blocks or call drop(guard) before calling any other function or method.
  2. Never invoke unknown callbacks while holding a Ref or RefMut: Clone or take the callback list out of the RefCell first, drop the guard, and then invoke the callbacks.
  3. Prefer Cell<T> when possible: Because Cell<T> never hands out inner references, .get(), .set(), .take(), and .replace() can never panic even under re-entrant calls.
  4. Use .try_borrow() and .try_borrow_mut() when contention is a valid runtime state that should be handled via Result.