RefCell<T> is a type that checks borrows at runtime.
Rust’s Borrow Checker is like a very strict traffic cop. Before letting you start the car (compile), it checks that all references are safe.
However, sometimes this cop is too conservative and rejects programs that are logically correct but difficult to analyze statically.
For these cases, Rust offers us RefCell<T>.
With RefCell, the traffic cop isn’t at the highway entrance (compile time), but instead rides along with you in the car (runtime). If you try to break the borrowing rules while driving, the program won’t fail at compile time, but it will panic (panic!) and crash while running.
The interior mutability pattern
Interior Mutability is a design pattern in Rust that allows mutating data inside an immutable structure.
RefCell<T> wraps your data and allows you to borrow mutable references (&mut T) even if the RefCell itself is immutable (&RefCell<T>).
How it works
RefCell has two main methods to access data:
.borrow(): Gives you a read reference (Ref<T>). You can have many..borrow_mut(): Gives you a write reference (RefMut<T>). You can only have one.
use std::cell::RefCell;
fn main() {
// Notice that 'box' is NOT 'mut'
let box = RefCell::new(5);
// We get mutable access via RefCell's checks
*box.borrow_mut() += 10;
println!("Value: {:?}", box.borrow()); // 15
}If we tried to do this with a Box or a normal variable without mut, the compiler would reject it. With RefCell, the validity of each borrow is checked when we call these methods.
The risk: runtime panics
RefCell keeps track of how many readers and writers are active while the program runs.
If you try to violate the rules (for example, having a borrow and a borrow_mut active at the same time), the program will crash.
let box = RefCell::new(5);
let write = box.borrow_mut(); // We hold the mutable borrow
let read = box.borrow(); // 💥 PANIC! A mutable borrow is already active.
// The program ends with:
// thread 'main' panicked at 'already borrowed: BorrowMutError'It is your responsibility as a programmer to ensure this doesn’t happen.
If the conflict might be an expected situation, try_borrow() and try_borrow_mut() return a Result instead of causing a panic.
Combining Rc<RefCell<T>>
In the previous article, we saw that Rc<T> allows multiple owners, but read-only.
Now we’ve seen that RefCell<T> allows interior mutability, but by itself does not share ownership.
What if we combine them? We get the best of both worlds: Multiple owners who can modify the data.
This combination appears in structures like graphs, trees with shared links, or certain observer patterns.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// 1. Create the data: An integer (5) inside RefCell (mutable),
// inside Rc (shared).
let value = Rc::new(RefCell::new(5));
// 2. Clone the Rc. Now we have 3 owners of the same data.
let a = Rc::clone(&value);
let b = Rc::clone(&value);
let c = Rc::clone(&value);
// 3. Modify the data through owner 'a'
*a.borrow_mut() += 10;
// 4. Read the data through owner 'b'
println!("b sees the value: {}", b.borrow()); // 15
println!("c sees the value: {}", c.borrow()); // 15
}Breaking down the layers
Rccontrols the life of the object (Reference Counting).RefCellcontrols access (Dynamic Borrow Rules).i32is the actual data.
When to use RefCell?
Don’t use it by default. It is appropriate when the model requires interior mutability and we can control the scope of dynamic borrows.
- Mock Objects in Tests: You want to test a function that modifies something internal, but the Trait signature says the argument is
&self(immutable). - Cache / Memoization: You want to save an expensive result inside a struct the first time it’s computed. From the outside, the method looks read-only (
get_value), but internally it needs to write to thecachefield. - Complex Data Structures: Graphs where nodes point to each other and change.
RefCell vs Cell
There is a lighter version called Cell<T>.
Cell<T>: Does not hand out references to its interior. Itsget()method requiresT: Copy, but it can also replace or extract non-Copyvalues through other methods. Since it doesn’t maintain dynamic borrows, these operations do not cause borrowing conflicts.RefCell<T>: Works for everything, uses references, and can panic.