TL;DR: Interior mutability is a design pattern that lets you mutate data even when you only hold an immutable (
&T) reference. Rust's borrow rules are enforced at compile time by default; interior mutability shifts enforcement to runtime. The main types are:Cell<T>(cheap, single-threaded,Copyvalues),RefCell<T>(single-threaded, borrow-checked at runtime), andMutex<T>/RwLock<T>(multi-threaded). Use the simplest type that fits your constraints.
Why Does Interior Mutability Exist?
Some valid programs cannot be expressed under Rust's compile-time borrow rules; interior mutability is the escape hatch.
The classic case: a graph or tree where nodes need to mutate their neighbors, but the borrow checker cannot statically prove the borrows do not overlap. Another case: implementing impl Trait methods that take &self but need to update internal caches or counters.
Rust's type system allows this because Cell, RefCell, and Mutex enforce the aliasing + mutation invariant at runtime rather than compile time; making them sound even if less efficient.
What Is Cell<T>?
Cell<T> provides interior mutability for Copy types with zero runtime overhead; no borrow checking, just direct value swaps.
use std::cell::Cell;
struct Counter {
value: Cell<u32>,
}
impl Counter {
fn increment(&self) { // takes &self, not &mut self
self.value.set(self.value.get() + 1);
}
}Cell works by copying values in and out; it never gives you a reference to its interior. This is why it only works for Copy types (or with .replace()/.take() for non-Copy).
What Is RefCell<T>?
RefCell<T> provides interior mutability for any type, enforcing Rust's borrow rules at runtime; it panics if you violate them.
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
{
let mut v = data.borrow_mut(); // runtime borrow check
v.push(4);
} // borrow released here
println!("{:?}", data.borrow()); // [1, 2, 3, 4]borrow() returns a Ref<T> (immutable). borrow_mut() returns a RefMut<T> (mutable). If you call borrow_mut() while any borrow is active, it panics at runtime.
What Is Mutex<T>?
Mutex<T> is the thread-safe version of interior mutability; it blocks until the lock is available, and is Send + Sync.
use std::sync::{Arc, Mutex};
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
std::thread::spawn(move || {
*c.lock().unwrap() += 1;
});RefCell is !Sync; never use it across threads. Use Mutex (exclusive access) or RwLock (multiple readers, one writer) for shared mutable state across threads.
Which Type Should You Use?
| Type | Thread-safe | Works with | Cost |
|---|---|---|---|
Cell<T> | ❌ | Copy types | Zero |
RefCell<T> | ❌ | Any type | Runtime borrow counter |
Mutex<T> | ✅ | Any Send type | OS lock (blocking) |
RwLock<T> | ✅ | Any Send + Sync | OS lock (readers share) |
AtomicUsize etc. | ✅ | Primitives only | Lock-free |
Use Cell → RefCell → Mutex in that order of preference.
Frequently Asked Questions
No; RefCell is !Sync, so the compiler prevents it from being shared across threads. It can only cause panics (at runtime), not undefined behavior.
The classic single-threaded shared mutable ownership pattern: Rc for multiple owners, RefCell for mutability. For multi-threaded code, use Arc<Mutex<T>> instead.
No. The invariant (no simultaneous aliased mutation) is still enforced; just at runtime. The unsafe keyword is not needed; the types handle it internally.
Sources
Related Glossary Terms
- refcell: Deep dive into
RefCell<T> - mutex: Thread-safe interior mutability
- rc: Often paired with
RefCellfor shared ownership - Ownership: Interior mutability is one of Rust's key ownership escape hatches
Keep Reading
- Rust Ownership and Borrowing: Interior mutability only makes sense after understanding the borrow checker

