RefCell<T> and Interior Mutability
Rust's borrow checker enforces its rules at compile time: you can have many immutable references or exactly one mutable reference, but never both at the same time.
Sometimes you have code where you know the borrow rules are satisfied, but the compiler cannot prove it statically. Interior mutability is a design pattern that moves borrow checking to runtime, letting you mutate data even through what looks like an immutable reference.
RefCell<T> is the standard runtime borrow checker in Rust's standard library.
How RefCell Works
RefCell<T> wraps a value and tracks borrows at runtime using an internal counter.
It exposes two main methods:
borrow()— returns aRef<T>(immutable access). Multiple immutable borrows can coexist.borrow_mut()— returns aRefMut<T>(mutable access). Only one mutable borrow may exist at a time, and no immutable borrows may be active simultaneously.
If you violate the rules — for example calling borrow_mut() while an immutable
borrow is active — RefCell panics at runtime rather than failing at compile time.
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// Immutable borrow — works fine
{
let view = data.borrow();
println!("data: {:?}", *view);
} // view dropped here, borrow released
// Mutable borrow — works because no other borrow is active
{
let mut view = data.borrow_mut();
view.push(4);
} // mutable borrow released
println!("after push: {:?}", data.borrow());
}data: [1, 2, 3] after push: [1, 2, 3, 4]
Runtime Panic on Borrow Violation
Unlike compile-time borrow errors, a RefCell violation crashes the program at the
point where the conflicting borrow is attempted.
use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let _borrow1 = data.borrow(); // immutable borrow — OK
let _borrow2 = data.borrow(); // second immutable borrow — also OK
// This panics: cannot borrow mutably while immutable borrows exist
let _mut_borrow = data.borrow_mut(); // PANIC at runtime
}RefCell panic is a program bug, not a graceful error. Structure your code so borrows are short-lived (released before the next one begins). Use try_borrow() and try_borrow_mut() if you need to handle the conflict non-fatally.try_borrow and try_borrow_mut
The non-panicking variants return a Result instead of panicking. Use them when
the borrow pattern depends on runtime state and a crash would be unacceptable.
use std::cell::RefCell;
fn main() {
let data = RefCell::new(100);
let borrow = data.borrow();
// try_borrow_mut returns Err if a borrow rule would be violated
match data.try_borrow_mut() {
Ok(mut val) => *val += 1,
Err(e) => println!("could not borrow mutably: {}", e),
}
drop(borrow); // release the immutable borrow
// Now the mutable borrow succeeds
match data.try_borrow_mut() {
Ok(mut val) => {
*val += 1;
println!("new value: {}", *val);
}
Err(e) => println!("error: {}", e),
}
}could not borrow mutably: already borrowed: BorrowMutError new value: 101
The Classic Pattern: Rc<RefCell<T>>
Rc<T> provides shared ownership but only immutable access.
RefCell<T> provides interior mutability for a single owner.
Combining them — Rc<RefCell<T>> — gives you shared mutable data on a single
thread. This is one of the most commonly used patterns in Rust for building graphs,
trees, and other data structures where multiple nodes need to mutate shared state.
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let shared = Rc::new(RefCell::new(vec![]));
// Clone the Rc — both handles point to the same RefCell<Vec>
let clone1 = Rc::clone(&shared);
let clone2 = Rc::clone(&shared);
clone1.borrow_mut().push(1);
clone2.borrow_mut().push(2);
shared.borrow_mut().push(3);
println!("{:?}", shared.borrow()); // [1, 2, 3]
println!("owners: {}", Rc::strong_count(&shared)); // 3
}[1, 2, 3] owners: 3
borrow() and a borrow_mut() at the same time. Structure your code so borrows are released (go out of scope or are explicitly dropped) before the next one is acquired.Practical Example — Shared Mutable Counter
use std::rc::Rc;
use std::cell::RefCell;
fn increment(counter: &Rc<RefCell<i32>>, amount: i32) {
*counter.borrow_mut() += amount;
}
fn main() {
let counter = Rc::new(RefCell::new(0));
let c1 = Rc::clone(&counter);
let c2 = Rc::clone(&counter);
increment(&c1, 10);
increment(&c2, 5);
increment(&counter, 3);
println!("final count: {}", counter.borrow()); // 18
}final count: 18
Cell<T> — Simpler Interior Mutability for Copy Types
Cell<T> is a lighter-weight alternative to RefCell<T> for types that implement
Copy (integers, booleans, chars, etc.). Instead of handing out borrow guards, it
works by copying the value in and out:
cell.get()— returns a copy of the inner value.cell.set(value)— replaces the inner value.
use std::cell::Cell;
struct Config {
value: i32,
access_count: Cell<u32>,
}
impl Config {
fn new(value: i32) -> Self {
Config { value, access_count: Cell::new(0) }
}
fn read(&self) -> i32 {
// &self is immutable, but Cell lets us update the counter
self.access_count.set(self.access_count.get() + 1);
self.value
}
}
fn main() {
let cfg = Config::new(42);
println!("read: {}", cfg.read());
println!("read: {}", cfg.read());
println!("read: {}", cfg.read());
println!("access count: {}", cfg.access_count.get()); // 3
}read: 42 read: 42 read: 42 access count: 3
Cell<T> over RefCell<T> for Copy types — it has no borrow tracking overhead and can never panic.Using RefCell in Tests — Mocking
One of the most practical uses of RefCell is writing test mocks. A mock needs to
record calls made to it, but it is typically accessed through a shared immutable
reference (e.g. a trait object). RefCell lets the mock mutate its internal log
even through that immutable interface.
use std::cell::RefCell;
trait Logger {
fn log(&self, message: &str);
}
struct MockLogger {
messages: RefCell<Vec<String>>,
}
impl MockLogger {
fn new() -> Self {
MockLogger { messages: RefCell::new(vec![]) }
}
fn logged_messages(&self) -> Vec<String> {
self.messages.borrow().clone()
}
}
impl Logger for MockLogger {
fn log(&self, message: &str) {
// &self is immutable, but RefCell lets us mutate the vec
self.messages.borrow_mut().push(message.to_string());
}
}
fn run_process(logger: &dyn Logger) {
logger.log("process started");
logger.log("process finished");
}
fn main() {
let logger = MockLogger::new();
run_process(&logger);
let msgs = logger.logged_messages();
println!("logged {} messages", msgs.len());
for msg in &msgs {
println!(" - {}", msg);
}
}logged 2 messages - process started - process finished
RefCell is NOT Thread-Safe
RefCell<T> uses a plain (non-atomic) integer to track borrows. Accessing it from
multiple threads simultaneously is a data race. The compiler enforces this — you
cannot send a RefCell to another thread.
For multi-threaded interior mutability, use Mutex<T> or RwLock<T> from the
standard library.
Type | Thread-safe | Borrow check | Panics | Use when |
|---|---|---|---|---|
RefCell<T> | No | Runtime | Yes on violation | Single-threaded interior mutability |
Cell<T> | No | N/A (Copy only) | Never | Single-threaded, Copy types |
Mutex<T> | Yes | Runtime (blocks) | No (returns Err on poisoning) | Multi-threaded mutation |
RwLock<T> | Yes | Runtime (blocks) | No | Multi-threaded, read-heavy mutation |
When to Use RefCell
You have a value that logically should be mutable but lives inside a type that is otherwise immutable (e.g. a cache, a counter, or a log).
You are combining with
Rcto getRc<RefCell<T>>— shared mutable data on a single thread.You are writing test mocks that must record calls through an immutable trait interface.
You are certain the borrow rules are satisfied at the call sites but the compiler cannot statically verify it.
RefCell<T> unlocks interior mutability when the compiler cannot prove borrow safety statically. Combined with Rc, it enables shared mutable state without reaching for unsafe code. Use try_borrow and try_borrow_mut in production code where a panic would be unacceptable.