When the borrow checker blocks your design
You are building a struct that caches a computed value. The public API takes &self because the caller doesn't need to mutate the struct, but the implementation needs to update the cache on the first call. You write the method, and the compiler rejects you with E0596 (cannot borrow as mutable). You can't change the signature to &mut self without breaking the API. You can't just add mut to the reference; the caller holds an immutable handle. You need a way to mutate data through an immutable window.
That is interior mutability. Rust enforces the rule that you cannot have mutable access and immutable access to the same data at the same time. RefCell<T> moves this enforcement from compile time to runtime. It wraps a value and tracks borrows dynamically, allowing mutation through an immutable reference while panicking if you violate the borrowing rules.
The runtime trade-off
Rust's borrow checker runs at compile time. It guarantees safety before the program starts. This gives you zero-cost abstractions and eliminates entire classes of bugs without runtime overhead. RefCell<T> trades that compile-time guarantee for flexibility. It allows patterns that the borrow checker cannot verify statically, such as mutation through &self or shared mutable state in single-threaded code.
The cost is a runtime check. Every borrow operation increments or decrements a counter. If you try to borrow mutably while an immutable borrow exists, or hold two mutable borrows, the program panics. You exchange a compile error for a runtime panic. This is a conscious design choice. You use RefCell when the borrow checker blocks a valid design, accepting the small performance cost and the risk of runtime failure in exchange for expressiveness.
The guard pattern: Ref and RefMut
RefCell does not return raw references. Calling borrow() returns a Ref<T>, and borrow_mut() returns a RefMut<T>. These are guard types that implement Deref and DerefMut. You can use them like references, but they carry the borrow state.
The guard keeps the borrow alive. When the guard goes out of scope, it decrements the borrow count. This mechanism is essential. If borrow() returned &T, the RefCell would lose track of the borrow after the call returns. The guard ensures the RefCell knows exactly when the borrow ends. You cannot return a Ref from a function that returns &T or T; the guard must be returned explicitly, or the borrow is scoped to the function body.
This design prevents dangling borrows. The guard ties the lifetime of the borrow to the lifetime of the guard variable. Scope the guard tightly, and the borrow ends immediately. Let the guard live too long, and you block other borrows, potentially causing panics or deadlocks in logic.
Minimal example
use std::cell::RefCell;
fn main() {
// RefCell::new wraps the value. The RefCell itself can be immutable.
let data = RefCell::new(42);
// borrow_mut() checks the runtime borrow count.
// It returns a RefMut guard. Deref coercion allows assignment.
// This panics if any borrow is currently active.
*data.borrow_mut() = 100;
// borrow() returns a Ref guard.
// This panics if a mutable borrow is active.
println!("Value: {}", *data.borrow());
}
The RefCell lives on the stack here, but the wrapper works the same way on the heap. The inner value can be any type. The RefCell adds a small amount of overhead for the borrow counters. The guards drop automatically when they go out of scope, releasing the borrows.
Realistic example: A caching struct
Interior mutability shines when you need to maintain internal state without exposing mutability to the caller. A cache is a classic use case. The struct computes a value on demand and stores it. Subsequent calls return the cached value. The public method takes &self.
use std::cell::RefCell;
/// A cache that computes a value once and stores it.
struct LazyCache {
// RefCell allows mutation through &self.
value: RefCell<Option<u32>>,
}
impl LazyCache {
fn new() -> Self {
LazyCache {
value: RefCell::new(None),
}
}
/// Get the cached value, computing it if necessary.
/// Takes &self, not &mut self.
fn get(&self, compute: impl FnOnce() -> u32) -> u32 {
// Check if the value is already cached.
// borrow() returns a Ref guard.
if let Some(v) = *self.value.borrow() {
return v;
}
// Compute the value.
let v = compute();
// Update the cache.
// borrow_mut() returns a RefMut guard.
// The guard drops at the end of this statement.
*self.value.borrow_mut() = Some(v);
v
}
}
fn main() {
let cache = LazyCache::new();
// First call computes the value.
let v1 = cache.get(|| {
println!("Computing...");
42
});
// Second call returns the cached value.
let v2 = cache.get(|| {
println!("This should not print.");
99
});
println!("v1: {}, v2: {}", v1, v2);
}
The RefCell hides the mutability. The caller sees an immutable LazyCache, but the struct updates its internal state. The guards scope automatically. self.value.borrow() drops at the end of the if line. self.value.borrow_mut() drops at the end of the assignment. This keeps the borrow windows narrow and safe.
Pitfalls: Panics, recursion, and scope
RefCell panics on violation. This is a runtime error. If you hold a Ref and try to borrow_mut, the program crashes with "already mutably borrowed". If you hold a RefMut and try to borrow, it crashes with "already borrowed". These panics indicate a logic error. You are trying to alias data in a way that violates Rust's safety rules.
Recursion is a common trap. A method that borrows mutably and then calls another method that also borrows mutably will panic. The first borrow is still active when the second borrow starts.
struct BadRecursive {
data: RefCell<u32>,
}
impl BadRecursive {
fn update(&self) {
// borrow_mut returns a guard.
let guard = self.data.borrow_mut();
// This call tries to borrow_mut again.
// The first guard is still alive.
// PANIC: already mutably borrowed.
self.helper();
}
fn helper(&self) {
let _ = self.data.borrow_mut();
}
}
Fix this by scoping the guard tightly. Extract the value or use a block.
fn update_fixed(&self) {
// Extract the value. The guard drops immediately.
let current = *self.data.borrow_mut();
// Now the borrow is released.
self.helper();
}
Another pitfall is storing guards in structs. Ref and RefMut are temporary guards. They track the borrow state. Storing them in a struct breaks the tracking. The RefCell expects the guard to drop to release the borrow. If you store the guard, the borrow stays active forever, blocking all future access. Never store Ref or RefMut in a struct. Use them only as local variables.
try_borrow and try_borrow_mut return Result instead of panicking. Use these in libraries where panics are unacceptable. They allow you to handle borrow failures gracefully.
Convention: Rc<RefCell> and graphs
The community often pairs RefCell with Rc. Rc<T> provides shared ownership, but the value is immutable. Rc<RefCell<T>> gives shared ownership with interior mutability. This pattern is standard for graphs, trees, and UI state where multiple nodes reference shared data that needs to change.
The convention is to use Rc::clone(&node) to clone the Rc, not the inner data. Rc::clone bumps the reference count. It does not copy the value. The explicit form signals that you are cloning the reference, not the data. node.clone() compiles but looks like a deep clone. Use Rc::clone to avoid confusion.
When working with Rc<RefCell<T>>, keep borrows short. Graph traversals often require borrowing multiple nodes. If you borrow a node mutably and then try to borrow a neighbor, you risk panics. Structure your code to release borrows before acquiring new ones. Use try_borrow to detect cycles or conflicts.
Decision: Choosing the right tool
Use &mut T when you have exclusive ownership and can express mutability at the call site. This is the most efficient and safest option. The compiler verifies everything at compile time with zero runtime cost.
Use RefCell<T> when you need interior mutability in single-threaded code and the value is large or expensive to copy. Reach for this when the API demands &self but the logic requires mutation, or when multiple owners share data via Rc.
Use Cell<T> when the value implements Copy and you need simple interior mutability without borrow tracking. Cell provides get and set methods. It does not track borrows, so it has lower overhead than RefCell. Use this for flags, counters, or small values where aliasing is not a concern.
Use Mutex<T> when you need interior mutability across threads. RefCell is not thread-safe. Mutex provides the same runtime borrow checking but with atomic operations for thread safety. The overhead is higher due to synchronization primitives.
Use Rc<RefCell<T>> when multiple owners need to share and mutate data in a single thread. This is the standard pattern for graphs and shared state in single-threaded applications.
Prefer &mut T. Reach for RefCell when the API demands &self but the logic demands change.