21.6RefCell and interior mutability

Last updated June 13, 2026

Chapter 9 gave you a rule with no exceptions: at any moment a value may have many shared references (&T) or one mutable reference (&mut T), never both. The compiler enforces it, and code that breaks it doesn't build. That rule is the foundation of Rust's safety, and you should keep trusting it. But it's enforced at compile time, which means the compiler must be conservative: it rejects anything it can't prove safe, even patterns that are actually fine. Interior mutability is the controlled escape hatch for those cases, and RefCell<T> is the tool. This also pays off a promissory note: lesson 5.1 named "interior mutability" as a thing static related to, and left it for later. Later is here.

The idea: same rule, checked at runtime

RefCell<T> holds a value and lets you borrow it mutably even when the RefCell itself is only shared. It doesn't abandon chapter 9's rule, many readers or one writer; it still enforces exactly that rule. The difference is when: RefCell checks it at run time instead of compile time. You ask a RefCell for a borrow with a method call, and it tracks the borrows itself; if you violate the rule, it doesn't fail to compile, it panics at the moment of the violation.

That trade, runtime check instead of compile-time check, buys flexibility (patterns the compiler couldn't prove become possible) at a cost (a small runtime overhead, and a violation becomes a panic rather than a build error). You reach for it only when you, the programmer, can see that an access is safe but the compiler can't.

borrow and borrow_mut

RefCell replaces & and &mut with two methods. borrow() gives a shared borrow (a Ref<T>, usable like &T); borrow_mut() gives a mutable borrow (a RefMut<T>, usable like &mut T). Here's mutation through a RefCell that is itself not declared mut:

use std::cell::RefCell;

fn main() {
    let cell = RefCell::new(5);   // note: not `let mut`

    *cell.borrow_mut() += 10;     // mutate through a shared RefCell

    println!("{}", cell.borrow());
}
15

Look at what just happened: cell isn't mut, yet we mutated the value inside it. That's the "interior" in interior mutability, the mutability lives inside the RefCell, not in the binding. borrow_mut() hands out a RefMut that we dereference with * to add 10; the borrow ends at the semicolon. Then borrow() reads it back. Each call checks, right then, that the borrowing rules are satisfied.

Break the rule, get a panic

The rule is still the rule. If you hold two mutable borrows at once, RefCell catches it, at run time, with a panic:

use std::cell::RefCell;

fn main() {
    let cell = RefCell::new(String::from("hi"));

    let mut first = cell.borrow_mut();
    let mut second = cell.borrow_mut();   // second mutable borrow: panics here

    first.push('!');
    second.push('?');
}
thread 'main' (12345) panicked at src/main.rs:8:30:
already borrowed: BorrowMutError

This is the same error chapter 9 would have caught at compile time (E0499, two mutable borrows), but because we used RefCell, it's deferred to run time and surfaces as a BorrowMutError panic the instant the second borrow_mut() runs. That's the cost of interior mutability laid bare: you trade a friendly compile error for a crash, and you take on the responsibility the compiler used to carry. Use RefCell only where the flexibility is worth that trade.

RefCell moves your bugs from compile time to run time

With ordinary &mut, a borrow-rule violation is a compile error: it can't ship. With RefCell, the same violation is a runtime panic: it compiles fine and blows up only when that code path runs, possibly in production, possibly only under load. That's strictly more dangerous, which is why RefCell is a tool of last resort, not a way to "turn off the borrow checker" when it's inconvenient. If & and &mut can express what you need, use them. Reach for RefCell only when a legitimate pattern genuinely requires runtime-checked borrowing.

The classic combination: Rc<RefCell>

RefCell becomes genuinely powerful paired with Rc from the last lesson. Recall the two halves: Rc<T> gives many owners but only shared (read-only) access; RefCell<T> gives mutation but only single ownership. Neither alone lets you have data that's both shared and mutable. Nest them, Rc<RefCell<T>>, and you get exactly that: multiple owners, each able to mutate the shared value.

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let shared = Rc::new(RefCell::new(vec![1, 2, 3]));

    let a = Rc::clone(&shared);
    let b = Rc::clone(&shared);

    a.borrow_mut().push(4);   // mutate through one handle
    b.borrow_mut().push(5);   // mutate through another

    println!("{:?}", shared.borrow());
}
[1, 2, 3, 4, 5]

shared, a, and b are three owners of the same RefCell<Vec<i32>> (the Rc layer). Through any of them, borrow_mut() reaches in and mutates the vector (the RefCell layer). Both pushes land on the one shared vector. This Rc<RefCell<T>> pattern, shared ownership of mutable data, is the standard building block for flexible, interconnected data structures in single-threaded Rust: a tree whose nodes can be edited through multiple references, an observer list, a shared cache. The borrow rules still hold; they're just enforced per-borrow_mut()-call at run time.

Key insight

The three smart pointers compose along two independent axes. Box<T>: one owner, mutate via normal &mut. Rc<T>: many owners, read-only. RefCell<T>: one owner, runtime-checked mutation. Rc<RefCell<T>>: many owners and runtime-checked mutation. You pick the combination by answering two questions, "how many owners?" and "do I need to mutate shared data?", and the borrow rules of chapter 9 are never actually broken, only moved to run time where RefCell is involved.

Quiz time

Question #1

What does RefCell<T> change about the borrowing rules, and what doesn't it change?

Show solution

It changes when the rules are enforced: RefCell checks the many-readers-or-one-writer rule at run time (via borrow()/borrow_mut()) instead of compile time, which lets you mutate through a shared RefCell and enables patterns the compiler couldn't prove safe. It does not change the rule itself, you still can't have a mutable borrow alongside any other borrow. Breaking the rule panics (BorrowMutError) instead of failing to compile.

Question #2

What is the downside of RefCell compared to ordinary &mut?

Show solution

A borrow-rule violation becomes a runtime panic instead of a compile error. With &mut, the bug can't ship; with RefCell, it compiles and only crashes when that code path runs (possibly in production). There's also a small runtime cost for the borrow tracking. So RefCell trades compile-time guarantees for flexibility, and should be used only when a pattern genuinely needs runtime-checked borrowing.

Question #3

Why combine Rc and RefCell as Rc<RefCell<T>>? What does each layer contribute?

Show solution

Rc<T> provides multiple ownership but only shared (read-only) access; RefCell<T> provides mutation but only single ownership. Neither alone gives shared and mutable data. Nesting them, Rc<RefCell<T>>, combines both: the Rc layer allows many owners (clone the handle), and the RefCell layer allows any owner to mutate the value via borrow_mut() (runtime-checked). It's the standard pattern for shared, mutable data structures in single-threaded code.

Rc and RefCell unlock flexible shared structures, but they also open the door to Rust's one memory-safety gap. The final lesson (21.7) shows how two Rcs pointing at each other can leak memory, and how Weak references break the cycle.