Rust Interior Mutability: Cell and RefCell
Understand Rust's interior mutability pattern using Cell and RefCell, when to use each, and how they let you mutate data behind shared references.
What you'll learn
- ✓What interior mutability means in Rust
- ✓How Cell works for Copy types
- ✓How RefCell adds runtime borrow checking
- ✓Combining RefCell with Rc for shared mutable state
Prerequisites
- •Basic Rust knowledge
- •Ownership and borrowing rules
Rust’s borrow checker enforces a simple rule: you can have either one mutable reference or any number of shared references, but never both at the same time. This works well most of the time, but some patterns need to mutate data through a shared reference. That is where interior mutability comes in.
Interior mutability is a design pattern that lets you mutate data even when there are immutable references to it. The standard library provides two primary types for this: Cell<T> and RefCell<T>. Both use unsafe code internally but expose a safe API.
Cell: Copy-Based Interior Mutability
Cell<T> works with types that implement Copy. Instead of giving you a reference to the inner value, it lets you get and set the value by copying it in and out.
use std::cell::Cell;
fn main() {
let value = Cell::new(42);
// Get copies the value out
println!("Before: {}", value.get());
// Set replaces the value
value.set(100);
println!("After: {}", value.get());
}
Because Cell never hands out references to its interior, there is no risk of dangling references or aliased mutation. The trade-off is that it only works with Copy types, so you cannot use it with String, Vec, or other heap-allocated types.
A practical use case is a counter that gets updated inside a closure that captures by shared reference:
use std::cell::Cell;
fn main() {
let call_count = Cell::new(0u32);
let increment = || {
call_count.set(call_count.get() + 1);
};
increment();
increment();
increment();
println!("Called {} times", call_count.get()); // 3
}
Another common pattern is a cached computation:
use std::cell::Cell;
struct Sensor {
raw_reading: f64,
calibration_offset: Cell<f64>,
}
impl Sensor {
fn new(raw: f64) -> Self {
Sensor {
raw_reading: raw,
calibration_offset: Cell::new(0.0),
}
}
fn calibrate(&self, offset: f64) {
// We can mutate even though self is &self, not &mut self
self.calibration_offset.set(offset);
}
fn reading(&self) -> f64 {
self.raw_reading + self.calibration_offset.get()
}
}
fn main() {
let sensor = Sensor::new(25.0);
sensor.calibrate(-1.5);
println!("Calibrated reading: {}", sensor.reading()); // 23.5
}
RefCell: Runtime Borrow Checking
RefCell<T> works with any type, including non-Copy types. It enforces the borrowing rules at runtime instead of compile time. You call borrow() to get a shared reference and borrow_mut() to get a mutable reference. If you violate the rules (for example, calling borrow_mut() while a borrow() is active), the program panics.
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// Immutable borrow
{
let borrowed = data.borrow();
println!("Length: {}", borrowed.len());
} // borrowed is dropped here
// Mutable borrow
{
let mut borrowed = data.borrow_mut();
borrowed.push(4);
}
println!("Final: {:?}", data.borrow()); // [1, 2, 3, 4]
}
If you try to borrow mutably while an immutable borrow is active, RefCell panics:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let _shared = data.borrow();
// This will panic at runtime!
// let _mutable = data.borrow_mut();
}
To avoid panics, you can use try_borrow() and try_borrow_mut(), which return a Result instead:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let _shared = data.borrow();
match data.try_borrow_mut() {
Ok(mut val) => *val = 100,
Err(e) => println!("Cannot borrow mutably: {e}"),
}
}
Combining Rc and RefCell
The most powerful pattern combines Rc<RefCell<T>>. Rc gives you shared ownership, and RefCell gives you interior mutability. Together, they let multiple owners mutate the same data.
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Database {
records: Vec<String>,
}
impl Database {
fn new() -> Self {
Database { records: vec![] }
}
fn insert(&mut self, record: String) {
self.records.push(record);
}
}
fn main() {
let db = Rc::new(RefCell::new(Database::new()));
// Multiple owners, all can mutate
let writer1 = Rc::clone(&db);
let writer2 = Rc::clone(&db);
writer1.borrow_mut().insert("Alice".into());
writer2.borrow_mut().insert("Bob".into());
println!("Database: {:?}", db.borrow());
}
This pattern is especially useful for tree and graph structures where nodes need to mutate their children or siblings:
use std::cell::RefCell;
use std::rc::Rc;
type Link = Option<Rc<RefCell<TreeNode>>>;
#[derive(Debug)]
struct TreeNode {
value: i32,
left: Link,
right: Link,
}
impl TreeNode {
fn new(value: i32) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(TreeNode {
value,
left: None,
right: None,
}))
}
}
fn main() {
let root = TreeNode::new(1);
let left = TreeNode::new(2);
let right = TreeNode::new(3);
root.borrow_mut().left = Some(Rc::clone(&left));
root.borrow_mut().right = Some(Rc::clone(&right));
// Mutate a child through shared access
left.borrow_mut().value = 20;
println!("Root: {:?}", root.borrow());
}
Cell vs RefCell: When to Use Which
| Feature | Cell<T> | RefCell<T> |
|---|---|---|
| Types supported | Copy types only | Any type |
| API | get() / set() | borrow() / borrow_mut() |
| Borrow checking | None needed (copies values) | Runtime |
| Panic risk | None | Yes, on borrow violations |
| Overhead | Very low | Small (tracks active borrows) |
| Thread-safe | No | No |
Use Cell when you are working with small Copy types like integers, floats, or booleans. It has less overhead and cannot panic.
Use RefCell when you need to mutate non-Copy types like String, Vec, or custom structs. Accept the runtime borrow-checking cost and be disciplined about keeping borrows short-lived.
Thread-Safe Alternatives
Neither Cell nor RefCell is thread-safe. For multi-threaded scenarios, use these alternatives:
Mutex<T>is the thread-safe equivalent ofRefCell. It locks the data for exclusive access and blocks other threads until the lock is released.RwLock<T>allows multiple readers or one writer, similar to Rust’s compile-time borrow rules but enforced at runtime with locking.AtomicU32,AtomicBool, etc. are thread-safe equivalents ofCellfor primitive types.
Wrapping Up
Interior mutability is Rust’s escape hatch for situations where the compile-time borrow checker is too strict. Cell handles Copy types with zero risk of panics. RefCell handles any type but moves borrow checking to runtime. Pair RefCell with Rc for shared mutable state in single-threaded code. When you move to multiple threads, switch to Mutex or RwLock. Use these types deliberately and keep their scope as small as possible.
Related articles
- Rust Rust Async Programming with Tokio: Getting Started
Learn async Rust with Tokio from scratch. Covers async/await syntax, spawning tasks, channels, TCP servers, and common pitfalls with working examples.
- Rust Rust Concurrency: Channels, Mutex, and Arc Patterns
Learn Rust concurrency with channels, Mutex, Arc, and thread-safe patterns. Covers mpsc, shared state, thread pools, and real-world examples.
- Rust Rust Error Handling with anyhow and thiserror
Master Rust error handling with anyhow for applications and thiserror for libraries. Practical patterns, conversions, and context-rich error reporting.
- Rust Rust Iterators and Closures: Functional Programming
Master Rust iterators and closures for functional-style programming. Covers map, filter, fold, chaining, custom iterators, and Fn trait family.