TL;DR:
RefCell<T>moves Rust's borrow-checking rules from compile time to runtime. It lets you obtain a mutable borrow (borrow_mut()) from an immutable reference; something the compiler normally forbids. This is safe becauseRefCellpanics if you violate the rules at runtime. Use it when you need mutability in a context where the compiler can't prove it's safe, and you're certain only one mutable borrow will exist at a time.
What Is Interior Mutability?
Interior mutability is a design pattern in Rust that lets you mutate data even when you only have an immutable reference to it; by moving borrow checking to runtime.
Normally, Rust enforces at compile time: either one mutable reference or any number of immutable references, never both. RefCell<T> enforces these same rules, but at runtime via a borrow counter inside the cell.
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// borrow(); immutable borrow (Ref<T>)
let r1 = data.borrow();
let r2 = data.borrow(); // Multiple immutable borrows: OK
println!("{:?} {:?}", *r1, *r2);
drop(r1);
drop(r2);
// borrow_mut(); mutable borrow (RefMut<T>)
data.borrow_mut().push(4);
println!("{:?}", data.borrow()); // [1, 2, 3, 4]
}What Happens If You Violate the Borrow Rules?
If you call borrow_mut() while an immutable or mutable borrow is active, RefCell panics at runtime with "already borrowed".
use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let _r = data.borrow(); // active immutable borrow
// PANIC: already borrowed: BorrowMutError
let _w = data.borrow_mut();
}Use try_borrow() and try_borrow_mut() to get a Result instead of panicking:
match data.try_borrow_mut() {
Ok(mut val) => *val += 1,
Err(_) => println!("already borrowed, skipping"),
}When Should You Use RefCell?
Use RefCell when you need mutation behind a shared reference; typically when combined with Rc<T> for single-threaded shared mutable state.
The classic pattern is Rc<RefCell<T>>:
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct SharedCounter {
count: i32,
}
fn main() {
let counter = Rc::new(RefCell::new(SharedCounter { count: 0 }));
let a = Rc::clone(&counter);
let b = Rc::clone(&counter);
a.borrow_mut().count += 1;
b.borrow_mut().count += 1;
println!("{:?}", counter.borrow()); // SharedCounter { count: 2 }
}Other valid use cases:
- Implementing graph/tree nodes with back-references
- Caching computed values in an otherwise immutable struct
- Mock objects in tests that need to record calls
How Is RefCell Different From Mutex?
RefCell is for single-threaded interior mutability. Mutex is for multi-threaded shared mutable access. They enforce the same borrow rules but in different contexts.
RefCell<T> | Mutex<T> | |
|---|---|---|
| Thread-safe | ❌ (!Send, !Sync) | ✅ |
| Borrow checking | Runtime (panics) | Runtime (blocking/async) |
| Combined with | Rc<T> | Arc<T> |
| Blocking | Never | May block (waiting for lock) |
| Overhead | Tiny counter increment | Lock acquisition (more expensive) |
// Single-threaded: Rc<RefCell<T>>
let shared = Rc::new(RefCell::new(0));
// Multi-threaded: Arc<Mutex<T>>
let shared = Arc::new(Mutex::new(0));What Are Cell<T> and OnceCell<T>?
Cell<T> and OnceCell<T> are lighter interior mutability primitives for simpler cases.
use std::cell::{Cell, OnceCell};
// Cell<T>; for Copy types, no borrow tracking needed
let x = Cell::new(5);
x.set(10);
println!("{}", x.get()); // 10
// OnceCell<T>; lazy initialization, write once
let cell: OnceCell<String> = OnceCell::new();
cell.set("hello".to_string()).unwrap();
println!("{}", cell.get().unwrap()); // "hello"Use Cell for simple Copy values. Use RefCell when you need borrows of non-Copy data. Use OnceCell for lazy one-time initialization.
Frequently Asked Questions
Yes; RefCell is 100% safe Rust. It cannot cause undefined behavior. The worst outcome is a panic at runtime if you violate borrow rules. This is why you should only use it when you can reason that borrows won't overlap.
With caution. Don't hold a RefCell borrow across .await; it's !Send and the executor may move the task to another thread. For async shared state, use tokio::sync::Mutex instead.
UnsafeCell<T> is the primitive building block; it's the only way to get a *mut T from a shared reference. Everything else (RefCell, Cell, Mutex) is built on top of it. You should never need UnsafeCell directly in safe code.
Try restructuring first. RefCell adds runtime overhead and panic risk. If restructuring your ownership to avoid shared mutability isn't feasible (e.g., callback-heavy code, recursive structures), then RefCell is the right tool.
Sources
- std::cell::RefCell: Standard library documentation
- The Rust Book; Interior Mutability
- Rust Reference; Interior Mutability
Related Glossary Terms
- Rc: Paired with RefCell for single-threaded shared mutable state
- Arc: Thread-safe counterpart; combined with Mutex instead
- Mutex: The multi-threaded alternative to RefCell
- Borrow Checker: The compile-time system RefCell defers to runtime
- Interior Mutability: RefCell is the canonical single-threaded interior mutability type
- RwLock:
RwLockis the multi-reader counterpart once code moves from single-threaded to multi-threaded mutation
Keep Reading
- Rust Ownership and Borrowing Explained: RefCell moves borrow checking from compile time to runtime
- Rust Lifetimes Deep Dive: interior mutability and its lifetime implications
