Skip to content

The Rust Engineering Handbook / Chapter 7

Shared and Mutable Borrowing

Reason about shared and exclusive access capabilities, split mutation domains safely, and keep borrows narrow.

Two indexes do not prove two mutation domains

An account transfer begins with an apparently simple requirement: subtract from one balance and add to another without ever losing or creating value. The records sit in one slice, identified by two runtime indexes. The direct attempt is rejected:

let first = &mut balances[i];
let second = &mut balances[j]; // E0499
*first -= cents;
*second += cents;

The programmer may already have checked i != j; the two indexing expressions do not carry that fact into the references they create. Rust sees two requests for exclusive access through one slice. Cloning the elements would avoid the rejection and quietly stop the transfer from updating its authoritative balances.

The useful model is capability, not syntax. A shared reference &T grants non-owning shared access subject to validity and the type’s permitted interior behavior. A mutable reference &mut T grants non-owning exclusive access for its active use. “Mutable” is not the whole story; exclusivity is what lets safe code update a region without conflicting access through another path.

Every reference must remain valid and properly aligned for its use. While an exclusive mutable reference is active, conflicting access to its referent is excluded. The design task is therefore to make the two mutation domains real, then keep both the borrowed regions and their active interval no larger than the transfer requires.

Shared and exclusive access answer different API questions

These signatures communicate distinct contracts:

fn total(entries: &[Entry]) -> i64;
fn normalize(entries: &mut [Entry]);
fn consume(entries: Vec<Entry>) -> Report;

The first borrows a slice for shared access and cannot move non-Copy entries out or ordinarily mutate them. The second exclusively borrows the slice and may update elements while preserving the caller’s ownership. The third consumes the vector and may retain, reorder, or destroy it.

Prefer the weakest capability that performs the job. This is not merely about making more call sites compile. A shared receiver signals observational work; an exclusive receiver signals mutation or another operation requiring exclusivity; an owned receiver signals lifecycle transfer.

Shared access does not mean “the bytes can never change.” Types built on interior mutability may change through &T, and external agents such as hardware may change memory under specialized contracts. Chapter 10 moves enforcement into runtime state; ordinary borrowing should not be described by pretending interior mutability does not exist.

Figure 7-1 separates the owner from the capabilities it grants. Thin and heavy arrows encode shared and exclusive access; the split boundary is a structural proof that the two mutable regions do not overlap.

A vector owner points to four account elements. Thin shared arrows may coexist over the same read-only region. A heavy exclusive arrow covers one mutable region and excludes conflicting arrows during its active interval. A split_at_mut boundary divides the vector into disjoint left and right slices, allowing one heavy arrow in each region. A timeline shows the exclusive borrow ending at its last use rather than necessarily at the closing brace.
Borrowing is a capability map over a valid region. Splitting a slice proves disjointness structurally, and narrowing last use returns capabilities to the owner sooner.

Borrows begin at creation and need last only through use

let account = &entry.account;
println!("{account}");
entry.cents += 25;

The shared borrow of the account field is needed through println!, not through the entire surrounding block. Non-lexical lifetime analysis often lets mutation follow the last use. It does not add runtime unlock operations; it is compile-time reasoning about where a borrow must remain valid.

Narrowing a borrow improves both compiler freedom and human review:

let is_system = entry.account.starts_with("system:");
if is_system {
    entry.tags.push("protected".to_owned());
}

Only the copied Boolean crosses the mutation. This is safe when the decision genuinely is a snapshot at that point. If the predicate must be checked atomically with mutation under concurrency, a local borrow rewrite is not a synchronization design.

Dereference and receiver adjustments preserve the capability

The unary * accesses the referent of a reference. Standard-library pointer types may participate through Deref and DerefMut; method calls can automatically borrow a receiver and apply dereference adjustments to find a method.

fn increment(value: &mut i64) {
    *value += 1;
}

An &mut i64 is itself a value. Reborrowing it for a call creates a shorter nested access rather than necessarily moving the original reference; Chapter 9 makes that pattern explicit.

Autoderef is ergonomic, but public APIs should not hide surprising ownership or expensive work behind dereference behavior. Deref should model pointer-like access, not general conversion. In review, resolve the receiver type, method source, and resulting borrow when a call’s capability is unclear.

Field disjointness can express the real mutation domain

Rust can often see that named fields do not overlap:

fn update(entry: &mut Entry) -> (&str, i64) {
    let account = &entry.account;
    entry.cents += 25;
    (account, entry.cents)
}

The returned shared reference points into account, while mutation targets cents. This is useful, but encapsulation can make methods borrow more broadly than their implementation needs. A method taking &mut self reserves the whole self at its API boundary even if it changes one field.

When this causes friction, compare three designs:

  1. Keep the method and shorten inputs before the call when the broad invariant is intentional.
  2. Split state into subobjects whose methods borrow only their own invariant domains.
  3. Use a free helper accepting explicit fields when no object-level invariant needs protection.

Do not expose private representation merely to win a borrow. The borrow boundary should match an invariant boundary, not compiler convenience.

Slice splitting gives runtime indexes a structural proof

This rejected program asks for two exclusive borrows through the same slice expression:

let first = &mut balances[i];
let second = &mut balances[j]; // E0499
*first -= cents;
*second += cents;

Even if an earlier check says i != j, independent indexing does not express the non-overlap in the reference construction. split_at_mut does. A useful helper must also preserve the caller’s requested order and reject bad indexes without panicking:

fn two_mut<T>(values: &mut [T], i: usize, j: usize) -> Option<(&mut T, &mut T)> {
    if i == j {
        return None;
    }

    let (low, high, reverse) = if i < j {
        (i, j, false)
    } else {
        (j, i, true)
    };
    if high >= values.len() {
        return None;
    }

    let (left, right) = values.split_at_mut(high);
    let low_value = &mut left[low];
    let high_value = &mut right[0];
    if reverse {
        Some((high_value, low_value))
    } else {
        Some((low_value, high_value))
    }
}

The split occurs at high. The lower element can only be in left; the higher element is the first item in right. Those slices are disjoint by the safe API’s contract, so the returned references can both be exclusive. reverse restores the requested (i, j) order after the proof has been constructed. The explicit bounds check works because low < high: if high exists, low exists too.

The helper proves only disjoint access. The transfer still owns its domain policy:

#[derive(Debug)]
struct Account {
    cents: i64,
}

#[derive(Debug, PartialEq)]
enum TransferError {
    SameAccount,
    MissingAccount,
    InvalidAmount,
    Arithmetic,
}

fn transfer(
    accounts: &mut [Account],
    from: usize,
    to: usize,
    cents: i64,
) -> Result<(), TransferError> {
    if from == to {
        return Err(TransferError::SameAccount);
    }
    if cents <= 0 {
        return Err(TransferError::InvalidAmount);
    }

    let (source, destination) =
        two_mut(accounts, from, to).ok_or(TransferError::MissingAccount)?;
    let source_after = source
        .cents
        .checked_sub(cents)
        .ok_or(TransferError::Arithmetic)?;
    let destination_after = destination
        .cents
        .checked_add(cents)
        .ok_or(TransferError::Arithmetic)?;

    source.cents = source_after;
    destination.cents = destination_after;
    Ok(())
}

Both arithmetic results are computed before either balance changes. An error therefore leaves the pair untouched; success changes one by -cents and the other by +cents, preserving their sum. Borrowing establishes that nobody else accesses either record through safe code during the update. It does not establish the business invariant by itself—the validation order and arithmetic policy do that.

Unsafe code can implement such splitting with raw pointers, but callers should prefer the safe standard operation. Reimplementing it expands the proof surface without adding product value.

Borrowing through methods affects the whole receiver contract

Receiver forms communicate capability. A method taking &self asks for shared access and may coexist with other shared borrows, though a type can explicitly signal interior mutation. A method taking &mut self reserves exclusive access to the receiver so it can mutate and re-establish the receiver’s invariants. A method taking self receives ownership and may retain, transform, or destroy the value. A receiver such as self: Box<Self> also makes the owned indirection part of that lifecycle boundary.

A getter returning &T couples the output validity to self and can keep part of the receiver borrowed. An iterator returning borrowed items can extend that relationship across a loop. Do not change a borrowed return to an owned clone merely to allow mutation; first decide whether callers need a snapshot or a live view.

Reference validity is an obligation for every use

A reference must point to a live, correctly aligned value of the referenced type for the relevant region, and safe code relies on Rust’s aliasing and validity rules. Moving the owner can be fine when the referent’s storage remains stable, as with moving a Box handle; moving a directly stored value can change its address. Safe APIs encode which cases are allowed.

Do not return a reference to a local:

fn bad() -> &str {
    let text = String::from("temporary");
    &text
}

The owner is destroyed at function exit, so no valid output relationship exists. Return the owned String, borrow from an input, or store the data in an owner that outlives the returned reference.

'static is not a repair for missing ownership. Chapter 8 distinguishes a reference to program-long data from a bound excluding borrowed dependencies.

Alternatives and production costs

For temporary observation, &T usually says exactly what the caller grants. Copying a small fact can deliberately shorten the borrow, but the result is a snapshot and may become stale. For in-place updates, &mut T preserves caller ownership; consuming and returning T makes a larger lifecycle transition explicit.

When two elements of a collection must change together, split_at_mut creates simultaneous disjoint access. A command expressed as indexes or handles can instead let the collection process the changes sequentially, which often preserves encapsulation but may not support an operation that genuinely needs both references at once. Work that must outlive the caller generally needs an owned command or intentional shared ownership such as Arc<T>, accepting allocation, atomic reference-count traffic, and a shared lifecycle.

Mutation behind shared access is a different contract. First ask whether the ownership domains should be redrawn. If shared mutation is real, Cell, RefCell, or a lock moves enforcement to runtime or synchronization, with failure, reentrancy, or contention costs developed in Chapter 10.

Borrowing has no reference-count increment and does not by itself allocate, but it constrains which accesses may overlap and couples API lifetimes. Choose by lifecycle and contention, then measure relevant hot paths.

Failure modes

  • Cloning authoritative records, then mutating the clone instead of the source.
  • Widening a method to &mut self when only a narrow component owns the invariant.
  • Holding a borrow across logging, callbacks, or unrelated computation.
  • Assuming distinct runtime indexes automatically produce disjoint borrows.
  • Calling shared access “immutable bytes,” ignoring interior mutability.
  • Using unsafe pointer arithmetic to replace a safe slice split.
  • Returning borrowed views when consumers require independent storage and lifecycle.

Senior review checklist

  • Is each parameter’s weakest sufficient capability visible in its type?
  • Does an &mut self receiver protect a real whole-object invariant?
  • Are shared borrows shortened before mutation without changing decision semantics?
  • Are runtime-disjoint collection regions expressed with safe splitting APIs?
  • Does every returned reference clearly borrow from an input or persistent owner?
  • Could a callback run while an exclusive borrow remains active?
  • Is interior mutation signaled rather than smuggled through an observational API?
  • Are bounds, panic, and concurrency implications documented?

Engineering exercise: settle two accounts

Extend transfer with an overdraft rule and an audit record returned only on success. Test both index orders, equal and out-of-range indexes, arithmetic overflow, insufficient funds, and conservation of the two-account total. In every error test, assert that neither account changed.

Then produce a command-based alternative in which the collection owns the accounts and accepts Transfer { from, to, cents } without returning references. Compare the two APIs: where is disjointness proved, which invariant boundary owns validation, and could a callback observe a half-applied transfer? Merely copying the helper cannot answer the callback question; you must decide when outside code may run.

Durable takeaways

  1. &T and &mut T are non-owning capability values; exclusivity is the defining power of mutable borrowing.
  2. A borrow need only remain active through the uses that depend on it, though the referent must remain valid for the full inferred relationship.
  3. Named fields and split_at_mut can prove disjoint mutation without cloning or unsafe code.
  4. Receiver choice is API design: it declares observation, exclusive mutation, or lifecycle transfer.
  5. Interior mutability is a separate enforcement mechanism, not an exception to be hidden inside the ordinary borrowing model.

References solve temporary access, but a returned reference also creates a provenance question: which input owner must remain valid while the caller uses it? Lifetime parameters make that relationship part of the API.

Sources and version notes