RefCell<T> and the Interior Mutability Pattern

Understand how RefCell<T> moves Rust borrowing checks to runtime, enabling interior mutability for patterns like mock objects, and how to combine it with Rc<T> for shared mutable data while avoiding memory leaks with Weak<T>

Rust’s ownership rules prevent you from mutating data through an immutable reference. That is a deliberate safety guarantee, but it creates a genuine problem: some designs need to record state or change a value inside a shared, otherwise immutable context. Test mocks, caches inside data structures, and tree nodes that need to mutate internal bookkeeping all run into the same wall. RefCell<T> is the standard library’s answer — a smart pointer that enforces the same borrowing rules, just at runtime instead of compile time, so code that logically obeys the rules can compile even when the compiler’s static analysis cannot prove it.

Enforcing Borrowing Rules at Runtime

RefCell<T> owns a single value, much like Box<T>. The difference is when the borrow rules are checked. With Box<T> (and ordinary references), the compiler verifies at build time that you never hold a mutable borrow and any immutable borrow simultaneously, and that all references are valid. That catches problems early and costs nothing at runtime. With RefCell<T>, the checks happen while your program is running. If you violate the rules — say by calling borrow_mut() while a borrow() is still active — the program panics with a BorrowMutError.

This trade-off lets you work in situations where the compiler is too conservative. Static analysis cannot decide every program’s correctness, and when you know the borrow rules are respected at runtime but the compiler disagrees, RefCell<T> bridges the gap. It is single-threaded only; using it across threads is a compile error, the same as Rc<T>.

A situation where the compiler says no

Take a simple violation that the compiler catches immediately:

fn main() {
    let x = 5;
    let y = &mut x;  // cannot borrow `x` as mutable
}

The fix is usually to declare x as mut, but what if you cannot change the signature? That is exactly the problem we face when implementing a trait with an &self method that must mutate internal state.

Mock objects that must record calls

Consider a library that tracks a value against a maximum and sends messages at certain thresholds. The trait Messenger defines how messages are sent, and the library expects an immutable reference to the messenger. In a test, we want the mock messenger to record the messages it receives — but the trait’s send method takes &self, not &mut self.

Here is the library code:

src/lib.rs
pub trait Messenger {
    fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
    messenger: &'a T,
    value: usize,
    max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
    T: Messenger,
{
    pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
        LimitTracker {
            messenger,
            value: 0,
            max,
        }
    }
    pub fn set_value(&mut self, value: usize) {
        self.value = value;
        let percentage_of_max = self.value as f64 / self.max as f64;
        if percentage_of_max >= 1.0 {
            self.messenger.send("Error: You are over your quota!");
        } else if percentage_of_max >= 0.9 {
            self.messenger
                .send("Urgent warning: You've used up over 90% of your quota!");
        } else if percentage_of_max >= 0.75 {
            self.messenger
                .send("Warning: You've used up over 75% of your quota!");
        }
    }
}

A naive mock implementation fails:

#[cfg(test)]
mod tests {
    use super::*;
    struct MockMessenger {
        sent_messages: Vec<String>,
    }
    impl MockMessenger {
        fn new() -> MockMessenger {
            MockMessenger {
                sent_messages: vec![],
            }
        }
    }
    impl Messenger for MockMessenger {
        fn send(&self, message: &str) {
            self.sent_messages.push(String::from(message)); // ❌ cannot borrow `self.sent_messages` as mutable
        }
    }
}

The error is clear: self is an immutable reference, so you cannot mutate sent_messages. Wrapping the vector in RefCell<T> solves it:

use std::cell::RefCell;
struct MockMessenger {
    sent_messages: RefCell<Vec<String>>,
}
impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        self.sent_messages.borrow_mut().push(String::from(message));
    }
}

borrow_mut() returns a smart pointer (RefMut<T>) that gives mutable access to the inner value, but it also registers the mutable borrow with the runtime borrow counter. When that RefMut goes out of scope, the counter is decremented. The test now works:

#[test]
fn it_sends_an_over_75_percent_warning_message() {
    let mock_messenger = MockMessenger::new();
    let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
    limit_tracker.set_value(80);
    assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}

Test Passes with RefCell:

The mock object now records messages through an immutable reference. This is interior mutability in action: the outer MockMessenger is not declared mut, but its internal sent_messages vector can still be mutated via RefCell.

Runtime enforcement and panics

RefCell’s safety net is not magic — breaking the rules still causes failure, just later. If you accidentally hold two mutable borrows at the same time, you get a runtime panic:

impl Messenger for MockMessenger {
    fn send(&self, message: &str) {
        let mut one_borrow = self.sent_messages.borrow_mut();
        let mut two_borrow = self.sent_messages.borrow_mut(); // panics: already borrowed
        one_borrow.push(String::from(message));
        two_borrow.push(String::from(message));
    }
}

Running cargo test produces:

thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at 'already borrowed: BorrowMutError'

Runtime panics replace compile errors:

With RefCell, violations that the compiler would normally reject become panic! at runtime. Always structure your scopes so that borrow() and borrow_mut() calls do not overlap unless you intend multiple immutable borrows. A single mutable borrow must be the only active borrow in its scope.

This example illustrates the core value of the interior mutability pattern: you uphold Rust’s safety guarantees yourself, at runtime, rather than relying on the compiler to prove them. The unsafe code that makes this possible lives inside RefCell’s implementation and is wrapped in a safe API.

Combining Rc<T> and RefCell<T>

Rc<T> lets you share ownership of immutable data between many parts of a program. RefCell<T> lets you mutate data through a shared reference. Together they become Rc<RefCell<T>> — a type that has multiple owners and allows each owner to mutate the value.

This pairing shows up frequently when building tree or graph structures. Each node needs to be referenced by its parent and potentially by several children, and you often want to modify a node’s children or metadata after it has been shared.

Here is a small node type that uses Rc<RefCell<T>> to build a mutable tree:

use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
    fn new(value: i32) -> Rc<Node> {
        Rc::new(Node {
            value,
            children: RefCell::new(vec![]),
        })
    }
    fn add_child(parent: &Rc<Node>, child: Rc<Node>) {
        parent.children.borrow_mut().push(child);
    }
}

A node is created inside an Rc so it can be shared. The children field is wrapped in RefCell so we can push new children even though parent is a shared &Rc<Node>. To add a child, we call borrow_mut() on the children RefCell, giving us a mutable reference to the underlying Vec, then push the new child’s Rc onto it.

Usage looks like this:

fn main() {
    let root = Node::new(1);
    let child_a = Node::new(2);
    let child_b = Node::new(3);
    Node::add_child(&root, child_a);
    Node::add_child(&root, child_b);
    // Read children back through an immutable borrow
    let children = root.children.borrow();
    for child in children.iter() {
        println!("Child value: {}", child.value);
    }
}

Because root is an Rc<Node>, we could clone it and hand the clone to another data structure, and both would see the same list of children. Modifications made through one handle are visible through all others. That is the power of combining shared ownership with interior mutability.

Single‑threaded only:

Rc and RefCell are both limited to single‑threaded scenarios. For multi‑threaded programs, replace Rc with Arc and RefCell with Mutex or RwLock. This chapter focuses on single‑threaded patterns; the concurrency chapter covers thread‑safe counterparts.

A common beginner mistake is to call borrow_mut() and forget that the returned RefMut will hold the borrow until it goes out of scope. If you inadvertently keep a RefMut alive while trying to call borrow() or another borrow_mut() on the same RefCell, the program panics at runtime. Always structure your code so mutable borrows are as short‑lived as possible, typically by enclosing them in a small block.

Reference Cycles and Memory Leaks

Rc<T> uses reference counting to decide when to drop the value. Each clone increases the strong count; each drop of an Rc decreases it. When the count hits zero, the value is freed. This works perfectly for acyclic ownership graphs, but nothing stops you from creating a cycle where two Rcs point to each other. In that case the strong count never reaches zero and the memory leaks.

Consider a naive doubly‑linked node where both parent and child own each other via Rc:

use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Option<Rc<Node>>>,
    children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
    fn new(value: i32) -> Rc<Node> {
        Rc::new(Node {
            value,
            parent: RefCell::new(None),
            children: RefCell::new(vec![]),
        })
    }
    fn add_child(parent: &Rc<Node>, child: &Rc<Node>) {
        parent.children.borrow_mut().push(Rc::clone(child));
        *child.parent.borrow_mut() = Some(Rc::clone(parent));
    }
}

When add_child runs, the parent stores a strong reference to the child, and the child stores a strong reference back to the parent. After both Rc pointers go out of scope, their strong counts are still at least 1 because they reference each other. The nodes are unreachable but never deallocated.

Cycles cause silent memory leaks:

Rust’s safety guarantees prevent dangling pointers and data races, but they do not prevent memory leaks — especially from reference cycles. A cycle of Rc pointers will keep all involved allocations alive until the program exits. Tools like cargo clippy and careful design are your primary defenses.

This is not a theoretical edge case. Any graph structure that contains back‑edges — parent pointers in trees, siblings in doubly‑linked lists, nodes in a graph — is susceptible. The standard library provides Weak<T> precisely to break these cycles without giving up shared ownership entirely.

Preventing Cycles with Weak<T>

Weak<T> is a non‑owning reference to the value inside an Rc<T>. It is created by calling Rc::downgrade, and it does not increase the strong count. Because the value might have been dropped already, accessing it requires an explicit upgrade step: upgrade() returns an Option<Rc<T>>, which is None if the strong count has dropped to zero and the allocation is gone.

For the tree node example, the parent pointer is the natural place for a Weak<T>. A child should not keep its parent alive; the parent’s lifetime should determine the child’s lifetime. Rewriting parent as a Weak<Node> eliminates the cycle:

use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}
impl Node {
    fn new(value: i32) -> Rc<Node> {
        Rc::new(Node {
            value,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![]),
        })
    }
    fn add_child(parent: &Rc<Node>, child: &Rc<Node>) {
        parent.children.borrow_mut().push(Rc::clone(child));
        *child.parent.borrow_mut() = Rc::downgrade(parent);
    }
    fn parent_value(&self) -> Option<i32> {
        self.parent.borrow().upgrade().map(|p| p.value)
    }
}

Now when the last strong reference to the parent is dropped, the child’s Weak reference becomes invalid. upgrade() returns None, and the child can handle that gracefully — for example, by returning None from parent_value().

Here is a demonstration:

fn main() {
    let parent = Node::new(1);
    let child = Node::new(2);
    println!("Strong count before: {}", Rc::strong_count(&parent));
    Node::add_child(&parent, &child);
    println!("Strong count after: {}", Rc::strong_count(&parent));
    println!("Child's parent value: {:?}", child.parent_value());
    drop(parent);
    println!("After parent dropped: {:?}", child.parent_value());
}

The output:

Strong count before: 1
Strong count after: 1
Child's parent value: Some(1)
After parent dropped: None

The strong count does not increase when the child stores a Weak, and after the parent is dropped, the child’s back‑pointer becomes inert. No leak occurs.

Cycle prevented:

By replacing the parent’s strong Rc reference with a Weak<T>, the ownership graph becomes a directed tree again. Dropping the root triggers the cascade of drops that Rust’s ownership model expects. This is the standard technique for building graph‑like structures with Rc without leaking memory.

A practical rule: always ask whether a back‑edge should keep the target alive. If not, use Weak. Typical examples include parent pointers in trees, observer lists where the subject should not own the observers, and cache entries that should expire when the main owner is gone.

Summary

Interior mutability via RefCell<T> moves Rust’s borrow rules to runtime, letting you mutate data behind an immutable reference when you can prove, at runtime, that the rules hold. The mock object example shows exactly where this is needed: a trait method bound to &self that must still mutate internal state. Combining RefCell<T> with Rc<T> gives you multiple owners of mutable data, the foundation for many tree and graph structures. That combination introduces the risk of reference cycles, which Weak<T> resolves by providing a non‑owning back‑reference that can be upgraded on demand.