Skip to content

The Rust Engineering Handbook

Appendix B — Ownership and Borrowing Decision Tables

Choose moves, borrows, clones, and shared ownership by boundary contract, then diagnose borrow failures as ownership-graph problems.

Start with the API diff, not the compiler message:

fn normalize(record: Record) -> Record
fn label(record: &Record) -> &str
fn enqueue(queue: &mut Vec<Record>, record: Record)
fn share(record: Record) -> Arc<Record>

Each signature grants a different capability. normalize takes responsibility for a value and returns responsibility after mutation. label temporarily observes and returns a view coupled to the input. enqueue receives exclusive temporary access to one owner and permanent ownership of another value. share converts unique ownership into reference-counted shared ownership. None is universally “more idiomatic.” The right signature follows from who must retain, mutate, store, transfer, or concurrently access the value.

Use the tables as decision instruments. First state the boundary contract; then select the least powerful capability that satisfies it. A compiler error is evidence that the implemented ownership graph cannot satisfy all claimed capabilities simultaneously—not an instruction to add clone, 'static, or a smart pointer.

Boundary questions before choosing a type

Answer these in order:

  1. Must the callee retain the value after the call?
  2. Must the caller retain independent use after the call?
  3. Does the callee need mutation, and must the caller observe it?
  4. Is the returned value independent, or a view into an input?
  5. Can one owner dominate the value’s useful lifetime?
  6. Is access single-threaded, transferred between threads, or genuinely concurrent?
  7. What allocation, copy, indirection, atomic, contention, and API-coupling costs are acceptable?

The first four questions usually choose a signature. The last three test whether the local choice survives system architecture.

Parameter decision table

Callee’s real job Prefer Caller after return Cost and coupling Warning
inspect without retaining &T retains ownership borrow only do not require &mut T for logical convenience
mutate caller-owned value in place &mut T retains ownership and observes mutation exclusive borrow blocks aliases for its use keep borrow scope narrow
consume, transform, store, send, or drop T loses that value unless it cloned beforehand may move pointer-sized fields without heap copy taking ownership is an API promise, not an optimization trick
inspect string-like input across several owned/borrowed forms a justified view such as &str or constrained AsRef<str> retains source generic forms may increase monomorphization and API surface accept the concrete view unless caller flexibility is valuable
optionally receive a value Option<T> or Option<&T> according to retention follows inner mode explicit absence a sentinel weakens invariants
store callback for later owned closure with the required lifetime and Fn* contract captured values follow closure capture may increase lifetime/dispatch coupling borrowed closure is only enough for call-scoped use
transfer across a thread/task boundary owned T satisfying that boundary’s trait/lifetime contract relinquishes or explicitly shares transfer may be zero-copy; sharing may be atomic 'static means no borrowed data shorter than the task, not “lives forever”

Prefer &[T] over &Vec<T> when the job is slice inspection, and &str over &String when the job is UTF-8 string inspection. These choices reduce representation coupling. Conversely, accept a concrete owner when capacity, allocator, layout, or ownership transfer is part of the contract.

Return decision table

Result contract Return shape Lifetime/API consequence Typical mistake
new independent result T caller owns and may retain freely returning a reference to a local temporary
view into one input &'a U tied to that input caller cannot outlive owner or conflicting mutation hiding which of several inputs supplies the view
may or may not find a view Option<&'a U> absence plus the same coupling cloning solely to avoid expressing the lifetime
choose a view from multiple inputs shared named lifetime when either may be returned output is limited by the intersection required at the call adding one lifetime to unrelated inputs without necessity
lazily yield borrowed items iterator with lifetime tied to owner owner remains borrowed while iterator is usable promising mutation while the iterator remains live
share one allocation among independent owners Arc<T> or Rc<T> where architecture requires it destruction occurs after last strong owner; cycles need design returning reference counting by default “for flexibility”
transfer optional failure detail Result<T, E> both branches are owned or deliberately borrowed returning borrowed error text from temporary formatting

Owned returns often simplify callers because they decouple lifetimes, but may allocate or copy. Borrowed returns can remove copying while exposing representation and restricting future mutation. Treat that restriction as part of public compatibility.

Move, borrow, clone, copy, or share

Operation New logical owner? Original usable? Runtime work Best fit
move T yes, responsibility transfers generally no; partially moved aggregates have limited use often field/register transfer, not deep copy handoff, storage, return, channel send
copy T: Copy yes, duplicated value yes bitwise duplication with type-defined semantics small independently meaningful values
shared borrow &T no yes, subject to borrow rules pointer/reference creation temporary observation
mutable borrow &mut T no owner resumes after exclusive use pointer/reference creation temporary in-place mutation
clone T: Clone yes, by type-defined duplication yes may allocate, traverse, increment counts, or be cheap intentional independent snapshot/ownership
Rc<T> clone another single-thread strong owner yes non-atomic count update shared immutable ownership within one thread
Arc<T> clone another thread-safe strong owner yes atomic count update; data mutation still needs a policy shared ownership across concurrency boundaries
Weak<T> non-owning upgradeable handle owners unaffected count update and fallible upgrade break cycles, registries, observers

Copy is implicit duplication; Clone is explicit but not necessarily deep. Arc<T> makes ownership sharing thread-safe, not mutation automatically safe or contention-free. Arc<Mutex<T>> combines shared ownership, runtime exclusivity, poisoning policy, lock scope, and contention; choose all of those contracts deliberately.

Diagnostic index: read the violated constraint

Compiler codes are useful search keys, but the graph violation is the durable model.

Symptom Violated ownership-graph constraint Questions to ask Credible repair families
use after move, often E0382 one non-Copy capability was transferred, then reused did the callee truly need ownership; should state be returned; is independent duplication intended? borrow; reorder final use; return ownership; clone with measured/semantic justification; redesign handoff
borrowed value does not live long enough, often E0597 a view edge would outlive its owner node who should own the data for the required duration; can the consumer finish sooner? shorten consumer lifetime; move owner outward; return owned data; store owner and offsets/keys instead of self-reference
cannot return reference to local, often E0515 return edge points into a node destroyed at function exit is result conceptually independent or a view of caller input? return owned value; borrow from an input; use caller-provided storage
mutable and shared borrow overlap, often E0502 exclusive capability overlaps another live capability where is the last real use; can read/compute/write be phased? narrow scopes; compute before borrowing mutably; split disjoint fields/slices; redesign method boundary
second mutable borrow, often E0499 two exclusive edges overlap are regions provably disjoint; does one operation retain the first borrow? split_at_mut; entry APIs; extract operation; stage mutation; reorganize data
closure may outlive borrowed value, often E0373 stored/spawned closure outlives captured borrow is transfer appropriate; who cancels and joins; what must remain caller-owned? scoped execution; move ownership deliberately; shared owner where real; redesign task boundary
value required for 'static boundary forbids short borrowed edges does work really outlive this scope; can ownership cross; can task be scoped? own captured data; use scoped API; revise boundary; avoid leaking merely to satisfy bound

Non-lexical lifetimes often end a borrow at its last use rather than the closing brace, but they do not prove domain-level disjointness the type system cannot see. Two-phase borrows permit selected receiver patterns; they are not permission to reason informally about arbitrary overlapping mutation.

Ownership-graph refactorings

Draw owners as boxes and borrows as time-bounded arrows. Then change topology, not punctuation.

Phase access instead of overlapping it

Before:

records ──shared view──> selected
   └────requested mutable update────X  (view still used later)

After:

records ──shared read──> compute owned key
[borrow ends]
records ──exclusive update by key──> result

This may require copying a small key, not cloning the whole record. The cost is explicit and the mutation phase is reviewable.

Split one owner into provably disjoint regions

Index arithmetic does not prove to Rust that two &mut slice[i] operations are disjoint. Use an API such as split_at_mut, which encodes that proof:

buffer owner
   ├── exclusive left region
   └── exclusive right region

The refactoring preserves one root owner while deriving nonoverlapping exclusive borrows through a trusted abstraction.

Move the owner to the lifetime root

Before, a function constructs data and tries to return a view into it. After, either return the owner or let the caller provide/own the storage:

bad: function-local String ──borrow──> returned &str
good: caller String ──borrow──> returned &str
good: function ──returns──> owned String

Choose borrowed output when caller storage is naturally authoritative. Choose owned output when independence is the real contract.

Replace internal references with stable coordinates

Self-referential structures are difficult because moving the owner may invalidate internal references and because mutation can invalidate elements. Store indexes, ranges, IDs, or arena handles, and resolve them through the owner at use time. This adds lookup and validation but removes a fragile lifetime edge. Pinning addresses movement only under a precise projection contract; it does not make arbitrary self-references safe.

Centralize ownership; lend views

If many components retain clones of large mutable state, define one authoritative owner and lend read models, commands, or snapshots. Message passing transfers owned commands and can make ordering/backpressure explicit. Shared state may be correct when access is truly concurrent, but then lock granularity, fairness, failure, and observation belong in the architecture.

Share immutable data only when independent lifetimes require it

Arc<T> is appropriate when no single owner naturally dominates all consumers and cross-thread owners must finish independently. Borrowed scoped threads or joined tasks may avoid it. If mutation is needed, consider copy-on-write snapshots, actor ownership, sharding, atomics for narrow state, or locks. The choice follows update semantics and contention evidence.

Repairs that compile but weaken the design

  • Cloning every input can hide an unclear transfer contract, amplify memory traffic, and produce stale divergent state.
  • Adding move to a closure may transfer more state than intended and can obscure shutdown or result ownership.
  • Requiring 'static in a public API can reject valid scoped borrowers and couple callers to allocation.
  • Leaking with Box::leak converts a lifetime error into deliberate process-lifetime retention; it is suitable only when that lifetime is truly intended and bounded.
  • Wrapping a value in Arc<Mutex<_>> can serialize unrelated operations, introduce poison/recovery choices, and make lock ordering part of correctness.
  • Replacing references with indexes can create stale-handle bugs unless generation, removal, and bounds are designed.
  • Broadening &T to &mut T for convenience excludes concurrent readers and falsely advertises mutation.

The borrow checker proves language-level aliasing and lifetime conditions for the written program. It does not prove that a clone is fresh enough, a lock is fairly used, an ID is authorized, a cycle cannot leak, or an ownership transfer matches the business invariant.

Applied design exercise

A configuration cache parses an owned String, serves named &str values, reloads periodically, and is read by worker threads. Design three versions:

  1. Single-request scope: parsing and consumption finish within one call. Use an owned parser buffer with borrowed views; state exactly where the owner lives.
  2. Immutable generation: workers may finish after reload. Use an Arc<Snapshot> generation, atomically or lock-protected swap of the current owner, and old-generation retention until readers finish. Account for atomic counts and peak memory.
  3. Independent results: callers retain selected values for an unbounded time. Return owned values or domain-specific owned types; compare allocation with lifetime decoupling.

For each version, draw owner nodes, borrow/share edges, mutation points, and destruction conditions. Reject one tempting repair and explain the cost it hides. Then encode the smallest version in the companion fixture by adapting Record, label, choose_label, normalize, and share.

Review card

At a signature or borrow-checker review, record:

  • authoritative owner and destruction point;
  • capability granted to each caller/callee;
  • whether retention, mutation, transfer, or concurrent independence is required;
  • output independence or exact input-lifetime coupling;
  • allocation, clone, indirection, atomic, contention, and peak-memory costs;
  • compiler rejection translated into a graph constraint;
  • at least two repair topologies and why one matches the domain;
  • concurrency, cancellation, panic, and cycle behavior where applicable;
  • executable positive evidence and preserved rejected-code evidence.

The companion crate at examples/rust-engineering-handbook/appendices/reference-lab/ checks borrowed inspection, owned transformation, explicit cloning, shared ownership, and intentional compile failures. When this appendix’s decision is “borrow,” carry the resulting owner and view edges into Appendix C; its catalog states the narrowest valid relationship among multiple input and output lifetimes.

Sources and version notes

  • The Rust Book’s ownership chapters introduce moves, borrowing, slices, and their compiler-checked rules.
  • The Rust Reference on expressions defines place/value contexts, moves, and borrowing behavior.
  • Standard-library documentation for Clone, Copy, Rc, and Arc states their API contracts.
  • Exact diagnostic codes and wording are current compiler behavior rather than stable language syntax. Diagnose the violated ownership constraint first and preserve minimal rejected programs for regression checking.
  • Recommendations about ownership graphs, API coupling, and refactoring are engineering guidance. Their fitness depends on workload, retention, concurrency, and failure requirements.