Skip to content

← Ownership II: Borrowing and the Borrow Checker step 22 of 24

Medium Primitives

Interior mutability: moving the check from compile time to run time

pub fn observe(ops: &[String]) -> (Vec<String>, bool)

Keep a log of strings. Two closures share it: one writes to it, one reads its length. Process each op in order and return (the finished log, whether an overlapping borrow was ever detected).

The ops, exactly:

op effect
"log <t>" append the text <t>
"count" read the current length n, then append "count=<n>"
"nested" take a shared guard on the log, read its length n, ask for an exclusive one while still holding it, record whether that failed, release the shared guard, then append "nested=<n>"
anything else append "? <op>"
[]                        -> ([], false)
["log a","log b"]         -> (["a","b"], false)
["log a","count"]         -> (["a","count=1"], false)
["nested"]                -> (["nested=0"], true)
["log x","nested","log y"]-> (["x","nested=1","y"], true)
["zzz"]                   -> (["? zzz"], false)

The "nested" op must report the conflict as true, not panic. That distinction is the whole point.

The starter tries to do this with two ordinary closures over a plain Vec and does not compile:

error[E0502]: cannot borrow `log` as immutable because it is also
              borrowed as mutable

which is the previous item’s lesson: two closures, one holding &mut log and one wanting &log, cannot coexist.

The trade you are about to make

Everything so far has been “the compiler proves it at compile time, for free”. This item is the first time you give that up, deliberately. It should feel like a trade, not an upgrade.

RefCell<T> implements the same rule — shared XOR exclusive — but enforces it with a runtime counter instead of a static proof:

  • .borrow()Ref<T>, incrementing a shared count.
  • .borrow_mut()RefMut<T>, taking the exclusive slot.
  • If the request conflicts with what is already out, it panics.
  • Guards decrement on Drop.

What you gain: shapes the checker cannot express — two closures both touching one value, a graph node reachable from several places, a cache inside an &self method.

What you pay:

  1. A counter incremented and checked on every borrow. Small, not zero.
  2. A panic path in production. borrow_mut() while a Ref is out will abort your program at runtime, on the machine of whoever is unlucky. The compiler will not warn you. Nothing will, until it happens.
  3. Errors move from your desk to your users. A borrow bug that was a red squiggle is now a 3am page.

That is a real trade and it is not always the wrong one. But “use RefCell“ is never the first answer to a borrow error. Work items 3.6, 3.15 and 3.21 first.

try_borrow_mut — the version that does not panic

match log.try_borrow_mut() {
    Ok(mut guard) => guard.push(line),
    Err(_) => { /* somebody else has it */ }
}

try_borrow and try_borrow_mut return Result instead of panicking. In library code, or anywhere a conflict is possible rather than impossible, they are the honest choice. This problem requires them: the "nested" op deliberately creates a conflict, and you must report it as a bool.

Cell versus RefCell

Both give you interior mutability. They do it in completely different ways, and picking the right one matters.

Cell<T> never hands out a reference at all. Its whole API is get (requires T: Copy), set, replace, take, update. Values go in and come out by copy or by move. Since no reference ever exists, there is nothing to alias, so there is no counter and no panic — ever. For a Cell<u32> counter or a Cell<bool> flag, this is strictly better.

RefCell<T> hands out Ref/RefMut guards, so it must count them at runtime, so it can panic. You need it when you want to operate on the value in place — pushing to a Vec, mutating a struct field — rather than swapping it wholesale.

Rule of thumb: if Cell can do the job, use Cell.

The panic messages, verified on 1.95

If you do trip a real panic, the two directions produce two different messages:

  • a failing borrow_mut() says RefCell already borrowed
  • a failing borrow() says RefCell already mutably borrowed

Most tutorials, and the Book, still quote the older already borrowed: BorrowMutError text. That is stale. So is the belief that both directions print the same thing. If you ever assert on this in a test, assert the message that matches the call that actually failed.

Two lints worth knowing

await_holding_refcell_ref — holding a Ref across an .await is a deadlock-by-construction in async code, because the task can be suspended with the guard still out. readonly_write_lock — you took a write lock and only read through it.

(significant_drop_tightening, which suggests dropping a guard earlier than scope end, is a nursery lint and allow-by-default; it will not fire on this gate.)

Implementation notes

Write the log as RefCell<Vec<String>> and let both closures capture it by shared reference — that is the point: &RefCell<T> gives you mutation, so two closures can share one. For the "nested" op, hold the Ref in a local, call try_borrow_mut(), and drop(guard) before you write, or your own subsequent write will conflict too. Finish with into_inner() to take the Vec back out.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…