---
title: 'Interior Mutability'
source: 'https://academia.sh/en/courses/ownership/interior-mutability'
course: 'Ownership and the Memory Model'
language: en
updated: '2026-09-15T04:58:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Runs code showing that the same "one writer or many readers" rule is tested at runtime instead of at compile time'
    - 'Proves with an example that the value inside a binding can be modified through RefCell while the binding itself stays immutable'
    - 'Shows that a failed borrow attempt can be taken as a bool result instead of a panic'
---

# Interior Mutability

How the same 'one writer or many readers' rule is tested with a counter at runtime instead of compile time is measured; that a binding itself can stay immutable while the value inside it becomes modifiable through RefCell, and that a failed borrow attempt can be taken as a bool result instead of a panic, are shown. The rule does not disappear, it changes location.

Across the previous three lessons a shared value could always be read; it could never be
modified in `Rc` or `Arc`, while in `Box` modification was already possible because there
was a single owner (if the binding took `mut`). The Variables and Immutability lesson had
also left a question open: it established that a binding not taking `mut` guarantees no
change is allowed **through that binding's own name**, but the question of whether the same
data could be shared through another binding or reference, and if shared, whether
immutability would still hold from there, was left to this course. This lesson answers
exactly that question: is there a way to modify a value both while ownership is shared and
while the binding looks immutable? What this course measures stays the same throughout —
where a value's life ends — not how a running unit stops, the way the Go curriculum's
Concurrency course measures.

The answer is yes, but this change has a cost; this lesson measures that cost, and as the
topic's fourth lesson, brings together the topic's four measurements so far.

The Atomic Reference Counting lesson left this need explicit: a line trying to write to a
field through an `Arc` did not compile, because it had been shown that knowing how many
copies can access a shared value at once is not enough to modify that value safely. The
same limit held for `Rc` — in `Box` modification was already possible because there was a
single owner (if the binding took `mut`), but in `Rc` and `Arc`, writing shut the moment
sharing began, because both gave only shared access to the value they held. In this lesson
that door reopens, but with a different key: not through the type itself, but through a
permission tested at runtime.

## The Rule Changes Location

The Borrow Checker lesson showed that the rule "either one mutable reference or several
shared references at a time" is tested at compile time: a piece of writing that violates
this rule never compiled at all. `RefCell<T>` is a container that carries the same rule to
**runtime**. Shared access to the value inside it is requested with `borrow()`, mutable
access with `borrow_mut()`; `RefCell` tracks these requests with a counter, and a request
that violates the rule is not left uncompiled, it is **rejected** at runtime.

In this lesson the rejection is measured not with a panic, but with a result returned by
`try_borrow()` and `try_borrow_mut()` calls: `Ok` if the request is valid, `Err` if not. No
panic text is printed anywhere; only whether the request succeeded is printed, as a `bool`.

`RefCell`, just like `Rc` and `Arc`, keeps a counter too — but what it counts is different.
`Rc` and `Arc`'s counter counted how many **owners** there were, and the value ended once it
hit zero. `RefCell`'s counter counts how many **active borrows** there are, and it never
ends the value; it only tracks which kind of access (shared or mutable) is currently
ongoing. These two counters can work independently of each other: an `Rc<RefCell<T>>` value
can hold both how many copies it has and who currently borrows it, at the same time, with
two separate counters. While one copy ends and the ownership counter falls, the borrow
counter is entirely unaware of whether that copy currently held a borrow — the two answer
separate questions.

## A Binding's Immutability Does Not Bind Its Content

The run below shows this directly: the binding named `counter` does **not** take `mut`, but
the integer inside it changes anyway.

**SO10.** — the claim that "the value inside can change while the binding itself stays
immutable" is confirmed by the run below: even though the line
`let counter: RefCell<i64> = RefCell::new(0);` does not contain `mut`, the line
`value after the first write: 1` is printed.

```rust
// borrow_time_shift.rs — a rule rejected at compile time turns into a result at runtime
use std::cell::RefCell;

fn main() {
    let counter: RefCell<i64> = RefCell::new(0); // binding is not mut

    {
        let mut writer = counter.borrow_mut();
        *writer += 1;
    }
    println!("value after the first write: {}", counter.borrow());

    let first_read = counter.borrow();
    let second_read = counter.borrow();
    println!("two reads together: {} and {}", *first_read, *second_read);

    let write_attempt = counter.try_borrow_mut();
    println!("did the write attempt succeed while reading was ongoing: {}", write_attempt.is_ok());

    drop(first_read);
    drop(second_read);

    let second_write_attempt = counter.try_borrow_mut();
    println!(
        "did the write attempt succeed after the reads were dropped: {}",
        second_write_attempt.is_ok()
    );
}
```

```
value after the first write: 1
two reads together: 1 and 1
did the write attempt succeed while reading was ongoing: false
did the write attempt succeed after the reads were dropped: true
```

The `counter` binding does not carry `mut` anywhere, but the `borrow_mut()` call genuinely
changes the value inside it. The Variables and Immutability lesson established that not
taking `mut` guarantees no change is allowed **through the name itself**; `RefCell` does not
break this guarantee — the name `counter` itself still cannot be rebound, none of its fields
can be changed with an ordinary assignment. `borrow_mut()` enters not through the name
itself, but through a separate gate `RefCell` checks at runtime. While `first_read` and
`second_read` live at the same time, both can read the same value — "many readers" is
permitted. A write attempt arriving while they are still alive fails; the same attempt
succeeds once both are dropped. The rule is the same: one writer or many readers, never
both.

## Two Writers Violate the Same Rule Too

The rule covers not only "no reading while writing" but also the part that says "no two
writers at the same time". The run below tests this separately: the same rejection happens
when a second mutable borrow is requested while a mutable borrow is already ongoing.

**SO11.** — the claim that "two mutable borrows at the same time are both rejected" is
confirmed by the `did the second write attempt succeed while the first writer was active:
false` line in the run below.

```rust
// two_writers_attempt.rs — two mutable borrows at the same time are both rejected
use std::cell::RefCell;

fn main() {
    let data: RefCell<Vec<i32>> = RefCell::new(vec![1, 2, 3]);

    let mut first_writer = data.borrow_mut();
    first_writer.push(4);

    let second_write_attempt = data.try_borrow_mut();
    println!(
        "did the second write attempt succeed while the first writer was active: {}",
        second_write_attempt.is_ok()
    );
    drop(first_writer);

    let is_it_successful_now = data.try_borrow_mut();
    println!("successful after the first writer is dropped: {}", is_it_successful_now.is_ok());
    drop(is_it_successful_now);
    println!("data length: {}", data.borrow().len());
}
```

```
did the second write attempt succeed while the first writer was active: false
successful after the first writer is dropped: true
data length: 4
```

The second attempt, opened while `first_writer` is still held, is rejected too — this time
not two colliding reads, but two write requests. An element having been pushed to `data`
through `first_writer` (`push(4)`) becomes permanent the instant the guard is dropped: the
length in the last line is four. The counter distinguishes not just how many readers are
active, but also the **kind** of active access — while a writer exists, neither another
writer nor a reader is accepted.

These two runs (the previous section's and this section's) together cover the whole rule:
many readers were permitted, while a reader with a writer, and two writers, were both
forbidden. Two of the three cases were measured by `try_borrow` or `try_borrow_mut`
returning `Err`, the remaining one by returning `Ok`; no panic text was printed in any of
them, no program crashed in any of them. The "modifying while reading" violation rejected
at compile time in the Borrow Checker lesson meets us here under the same name, but as an
`Err` at runtime.

## The Rule Does Not Disappear, It Changes Location

None of the three rejection examples in the Borrow Checker lesson entered a code block —
all of them were described in prose, with the accepted solution placed next to them; the
violating source never compiled. Here the same kind of violation — another access to a
value being requested while it is being written — genuinely **compiles**; it only produces
an `Err` at runtime. This does not mean the rule has disappeared: an error avoided at
compile time turns into a result at runtime. The write attempt line printing `false` in the
run above shows that a violation of the kind that would never compile in the Borrow Checker
lesson compiles here and is caught at runtime — the program does not crash, because the
rejection is turned into a `Result` by `try_borrow_mut`, instead of a panic.

This trade-off's cost is this: compile-time checking filters out a piece of writing before
it runs; runtime checking only catches the same violation once that line actually runs.
Wrong code no longer fails to compile — instead it reports its own failure while running —
but this report only happens once that line is reached; a run that never hits the violation
never shows the problem at all. The principle measured in the Borrow Checker lesson — "the
checker secures ownership discipline, not correctness" — goes one step further here: runtime
checking, too, only tests the **path that runs**, it cannot see a violation on a line the
code never reaches.

## The Ledger Itself Is Explained in This Lesson

This was exactly the question the Variables and Immutability lesson left to the Ownership
and the Memory Model course: when the same data is shared through another binding or
reference, does immutability still hold from there? The `ledger` used throughout every lesson
of this course carried the answer to this question implicitly from the start. `ledger` was a
value of type `Rc<RefCell<Vec<String>>>`. The Reference Counting lesson explained the `Rc`
half, this lesson explains the `RefCell` half: `ledger` was shared (`Rc`) and its content was
modifiable (`RefCell`) — using two nested containers provided both sharing and
modifiability at the same time.

**SO12.** — the claim that "a function taking `&Ledger` can still modify its content" is
confirmed by the run below: even though the `add_record` function's parameter is `&Ledger`
(not a mutable reference), the line `record count: 2` is printed.

```rust
// the_ledgers_interior_mutability.rs — the ledger type used throughout this topic worked this way
use std::cell::RefCell;
use std::rc::Rc;

type Ledger = Rc<RefCell<Vec<String>>>;

struct Trace {
    name: String,
    ledger: Ledger,
}

impl Trace {
    fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace {
            name: name.to_string(),
            ledger: Rc::clone(ledger),
        }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

fn add_record(ledger: &Ledger, text: &str) {
    // The ledger parameter is taken with &, not mut; the content still changes.
    ledger.borrow_mut().push(text.to_string());
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));
    add_record(&ledger, "added-by-hand");
    {
        let _measurement = Trace::new("added-on-drop", &ledger);
    }
    let record = ledger.borrow();
    println!("records: {}", record.join(", "));
    println!("record count: {}", record.len());
}
```

```
records: added-by-hand, added-on-drop
record count: 2
```

The `add_record` function takes `ledger: &Ledger` — that is, `&Rc<RefCell<Vec<String>>>`, an
ordinary shared reference. Despite this, the `ledger.borrow_mut().push(...)` call genuinely
adds an element to the list. This is the very thing every `Trace::drop` call has been doing
since the topic's first lesson: even though the `drop` function takes `&mut self`,
`self.ledger` could not be modified directly because it was an `Rc` — the line
`self.ledger.borrow_mut().push(...)` used not the access `&mut self` itself grants, but the
runtime access `RefCell` grants. Every run since the start of the topic was actually using,
in the background, the very mechanism this lesson measures — only this lesson names that
mechanism explicitly.

This also explains why `ledger` is defined as `Rc<RefCell<Vec<String>>>` rather than plain
`Vec<String>`. `Vec<String>` alone would be neither shareable nor modifiable from several
places — a function would need to either own it or be given an ordinary reference to it,
and calling `push` through an ordinary reference would require `&mut`. `Rc` brought
sharing, `RefCell` brought modifiability; together, even though only `&Ledger` was passed
every time `Trace::new` was called, every `Trace` could write its own end to the same list.
This topic's first three lessons only took up the `Rc` half of this nested container; this
lesson completes the other half.

## Access Is Not Transparent, It Requires an Explicit Call

The Heap-Allocated Single Ownership lesson showed that fields inside a `Box` can be
accessed directly: the writing `point.x` was the same whether the value was on the heap or
not. In the Atomic Reference Counting lesson, `shared.value` could also be read directly.
`RefCell` does not carry this transparency forward: the value inside it cannot be reached by
writing `cell.field` directly, `borrow()` or `borrow_mut()` must first be called to obtain a
guard value. This difference is not a coincidence — `Box`, `Rc`, and `Arc` knew at compile
time that access to the value inside was **always safe**, so they could hide that access.
`RefCell`, on the other hand, tests safety only at runtime, on every single call; this test
needs to be **visible** in the source, because the test can fail too.

This visibility takes concrete form in the `Result` value `try_borrow_mut` returns: while
writing to a `Box` field is an operation that never fails, writing to a `RefCell` is an
operation that is attempted and whose **result must be checked**. The code cannot hide this
difference — the calling side is forced to handle, on the very next line, whether its
request was accepted. This is the final step of a pattern the topic has followed across
three measurements: when an operation is certain and when it is conditional can be read
from the type that operation returns — `()` always succeeds, `Result` may succeed or may
fail.

With these four measurements together, the full picture of the topic emerges.
Heap-Allocated Single Ownership established that a value can sit on the heap while its owner
stays singular. Reference Counting opened that singularity into a countable plurality.
Atomic Reference Counting showed that this counting gives the same result in different
contexts too, but that shared data still stays unmodifiable. This lesson removed that last
constraint, at a cost: modification is now possible, but every attempt now returns a
result, and that result has to be checked. At no step across the topic was determinism
lost — every end, every counter change, every borrow attempt was produced in the same order,
with the same result, in the run.

## Summary

- `RefCell<T>` is a container that tests the "one writer or many readers" rule with a
  counter at runtime instead of compile time; the value inside it ends its life **with its
  container**.
- Even when a binding itself is not `mut`, the value inside it can change through
  `RefCell`; the name itself still cannot be rebound, the change passes through a separate
  gate.
- The same data can also be modified with `RefCell` when shared across several bindings
  (`Rc` copies); this answers the question the Ownership and the Memory Model course left
  open about "what happens to immutability under sharing" — the `ledger` type used since the
  start of the course was exactly this mechanism.
- A failed borrow attempt is measured not with a panic, but with a result returned by
  `try_borrow`/`try_borrow_mut`.
- The rule does not disappear, it changes location: a violation avoided at compile time
  turns into a result (`Err`) at runtime.
- Access through `RefCell` is not transparent; every access requires an explicit `borrow`
  call, because a call may not always succeed.

## Next Step

Across these four lessons a value's ownership multiplied, became countable, went atomic, and
finally became modifiable while shared — but one single assumption was never questioned the
whole time: the values being shared were independent of one another, none of them was
**holding onto** another. Every counter counted the copies remaining for its own value; no
copy held the counted value itself backward. The course's final question asks exactly this:
what if two values, through `RefCell`, each place an `Rc` inside the other and end up
holding onto each other? Can the counter still drop to zero then, or can it never drop at
all?
