When immutable references need to mutate
You have a struct representing a game character. The game loop passes an immutable reference to the character because multiple systems read its state. Suddenly, the character takes damage. You need to update the health field. The borrow checker blocks you with E0596 (cannot borrow as mutable). You need a way to mutate data through an immutable reference.
Rust calls this interior mutability. It lets you change the inside of a value even when you only hold &T. Rust provides two safe wrappers for this: Cell<T> and RefCell<T>. They solve the same problem but with very different trade-offs. Cell<T> swaps values by copying. RefCell<T> tracks borrows at runtime and panics if you break the rules. Picking the wrong one leads to panics or code that feels like a hack.
Copy swaps versus runtime loans
Cell<T> is a copy-based box. You can never hold a reference to the interior. You can only swap the entire value out or set a new one. The compiler enforces this by making Cell methods return copies of the value, not references. This means Cell works best for small, Copy types like integers, booleans, or enums. You read the value, modify it, and write it back. No references are ever exposed.
RefCell<T> is a runtime borrow checker. It lets you borrow the interior as &T or &mut T, just like normal references. The difference is the timing. Normal references are checked at compile time. RefCell checks the rules at runtime. It maintains a counter of active borrows. If you try to borrow mutably while an immutable borrow is active, the program panics. This keeps the memory safe, but it moves the safety check from compile time to runtime.
Think of Cell like a vending machine. You insert a request, you get a value out. You never see the inside mechanism. You can't hold onto the product while looking at the machine. Think of RefCell like a library book with a digital checkout system. You can borrow it to read or edit. The system tracks who has it. If you try to borrow it while someone else is using it, the system throws an error.
Minimal examples
Here is the basic usage pattern for both types. Notice how Cell uses get and set, while RefCell uses borrow and borrow_mut.
use std::cell::{Cell, RefCell};
fn main() {
// Cell: Copy in, copy out. No references allowed.
// Works best for Copy types like i32.
let score = Cell::new(0);
score.set(10);
// get() returns a copy of the value.
// You cannot hold a reference to the score.
println!("Score: {}", score.get());
// RefCell: Runtime borrow checks. References allowed.
// Works for any type, including non-Copy types like Vec.
let inventory = RefCell::new(vec!["sword"]);
// borrow_mut() returns a guard that tracks the mutable borrow.
// The guard implements DerefMut, so you can use * or methods.
let mut inv = inventory.borrow_mut();
inv.push("shield");
// borrow() returns a guard for immutable access.
// You can hold this reference while other immutable borrows exist.
println!("Inventory: {:?}", inventory.borrow());
}
How the runtime checks work
RefCell maintains two counters: one for immutable borrows and one for mutable borrows. When you call borrow, the immutable counter increments. When the Ref guard drops, the counter decrements. When you call borrow_mut, the code checks if both counters are zero. If they are, it sets the mutable counter to one and returns a RefMut guard. If the counters are not zero, the program panics.
This mechanism enforces the borrowing rules at runtime. You can have many immutable borrows, or one mutable borrow, but never both. The guards ensure the counters are updated correctly. If you drop a guard early, the borrow ends early. If you leak a guard, the borrow persists, which can cause deadlocks in your logic.
Cell has no counters. It has no guards. It just copies data. The safety comes from the fact that you never expose a reference. Since no reference exists, aliasing rules cannot be violated. The compiler knows that Cell::get returns a copy, so it allows mutation through &Cell<T>.
Under the hood: UnsafeCell
Both Cell and RefCell wrap the same underlying primitive: UnsafeCell<T>. UnsafeCell is the only type in Rust that allows interior mutability. It disables the compiler's aliasing optimizations for the wrapped data. This is unsafe because it breaks the rule that &T implies immutability.
Cell and RefCell are safe wrappers around UnsafeCell. Cell stays safe by copying values and never exposing references. RefCell stays safe by tracking borrows and panicking on violations. You should rarely use UnsafeCell directly. It requires you to write unsafe code and prove invariants manually. Use Cell or RefCell unless you are building a custom smart pointer or a high-performance data structure where the overhead of RefCell is unacceptable.
Treat UnsafeCell as the foundation. Cell and RefCell are the safe doors built on top of it.
Realistic usage and conventions
In real code, you often mix Cell and RefCell inside a struct. Use Cell for simple flags or counters. Use RefCell for complex data that needs to be borrowed.
use std::cell::{Cell, RefCell};
/// A widget that tracks its ID and a mutable name.
/// ID is swapped via Cell. Name is borrowed via RefCell.
struct Widget {
// id is a simple integer. We swap it, we don't need references.
// Cell is perfect here. Zero overhead, no runtime checks.
id: Cell<u32>,
// name might be long. We want to borrow it to print or modify.
// RefCell lets us hold a reference to the string.
name: RefCell<String>,
}
impl Widget {
fn new(id: u32, name: &str) -> Self {
Self {
id: Cell::new(id),
name: RefCell::new(name.to_string()),
}
}
/// Update the widget name.
/// We have &self, but RefCell allows mutation.
fn update_name(&self, new_name: &str) {
// borrow_mut() returns a RefMut guard.
// The guard tracks the mutable borrow at runtime.
let mut name = self.name.borrow_mut();
*name = new_name.to_string();
}
/// Increment the ID.
/// Cell::replace is preferred over get/set.
fn increment_id(&self) {
// replace swaps the value in one step.
// It avoids a window where the value is read but not yet updated.
// It is also slightly more efficient.
self.id.replace(self.id.get() + 1);
}
}
There is a community convention for Cell. Prefer Cell::replace over get followed by set. replace swaps the value in a single operation. It avoids a race window in single-threaded code where the value is read but not yet written. It also communicates intent more clearly. The community calls this the "atomic swap" pattern, even though Cell is not thread-safe.
For RefCell, keep the borrow guards as short as possible. The guards hold the borrow active. If you hold a borrow_mut guard across a function call, you block all other access. This can cause logic errors or panics if the called function tries to borrow the same RefCell. Drop the guard explicitly or let it go out of scope quickly.
Pitfalls and compiler errors
RefCell panics are the most common pitfall. If you violate the borrowing rules, the program crashes with already borrowed: BorrowMutError. This happens if you hold a mutable borrow and try to borrow again, or hold an immutable borrow and try to mutate. This is a runtime crash. It is better than undefined behavior, but it is still a crash. A RefCell panic means your logic is broken. Fix the logic, do not catch the panic.
Cell has limitations. You cannot hold a reference to the interior. If you need to pass the interior to a function that takes &T or &mut T, Cell will not work. You must use RefCell. Also, Cell requires the inner type to be Copy if you use get and set. If the type is not Copy, you must use replace or into_inner. Trying to call get on a Cell<String> fails with E0277 (trait bound not satisfied) because String does not implement Copy.
Another pitfall is overusing interior mutability. If you find yourself wrapping every field in RefCell, rethink your design. Interior mutability should be the exception, not the rule. It makes code harder to reason about because mutation can happen anywhere. Prefer plain &mut references when you can. Use interior mutability only when the borrow checker blocks a valid design, such as shared state in a graph or caching inside an immutable struct.
Decision matrix
Use Cell<T> when the value is Copy and you only need to swap values, never hold references. Use Cell<T> when you want zero runtime overhead and can accept the restriction of no interior references. Use RefCell<T> when you need to borrow the interior as &T or &mut T to pass to other functions. Use RefCell<T> when the value is not Copy and you need to mutate it through an immutable reference. Use RefCell<T> when you are building a data structure like a graph or tree where nodes share ownership or borrow each other. Reach for plain &mut references when you can; interior mutability should be the exception, not the rule.
Interior mutability is a workaround for the borrow checker. If you use it everywhere, the borrow checker is fighting you. Step back and check your data flow.