Skip to content
v0.1.1latest release34 skills2 categories4 sub-categories79 tested scripts157 reference docsContact us

Restructuring Patterns

Reference for rust-borrow-checker-error-explainer · 364 lines

Programming / Rustv1.0.0Updated OCT 21 20267 references5 scripts

Fixes from the top of the ladder: change the shape of the code so the borrows no longer overlap, without adding clones, reference counting or runtime checks. Every example compiles on stable Rust 2021; the rejected version is kept as a comment with its error code.

Contents

Shorten the borrow

Most E0502/E0499 errors disappear once the first borrow ends before the conflicting access. NLL already ends a borrow at its last use, so the job is to move that last use earlier or copy the value out.

rust
fn bump_first(v: &mut Vec<u32>) {
    // let first = &v[0];        // shared borrow...
    // v.push(*first + 1);       // E0502: ...still used by the argument
    let first = v[0];            // u32 is Copy: no borrow survives the statement
    v.push(first + 1);
}

fn log_len_then_clear(names: &mut Vec<String>) {
    let n = names.len();         // read what you need first
    names.clear();
    println!("cleared {n} names");
}

When the value is not Copy, clone only the small piece you need (let id = order.id.clone();), not the whole container.

Collect then apply

The iterator borrows the collection for the whole loop. Compute the changes into a separate Vec, end the loop, then apply them.

rust
#[derive(Debug, Clone)]
struct Job { id: u32, retries: u32, failed: bool }

fn schedule_retries(jobs: &mut Vec<Job>) {
    // for j in jobs.iter() { if j.failed { jobs.push(..) } }   // E0502
    let retries: Vec<Job> = jobs
        .iter()
        .filter(|j| j.failed && j.retries < 3)
        .map(|j| Job { id: j.id, retries: j.retries + 1, failed: false })
        .collect();
    jobs.retain(|j| !j.failed);
    jobs.extend(retries);
}

For maps, collect keys first: let stale: Vec<K> = map.iter().filter(..).map(|(k, _)| k.clone()).collect(); for k in stale { map.remove(&k); } (or simply map.retain(|_, v| !v.stale)).

Remove while iterating

GoalAPISince
Drop elements matching a predicateVec::retain, HashMap::retain, VecDeque::retain1.0 / 1.18 / 1.4
Mutate and maybe drop in one passVec::retain_mut1.61
Remove and keep the removed elementsVec::extract_if(range, pred)1.87
Remove a rangeVec::drain(range)1.6
Remove one element by index, order irrelevantVec::swap_remove(i)1.0
rust
fn split_expired(sessions: &mut Vec<(u64, String)>, now: u64) -> Vec<(u64, String)> {
    let mut expired = Vec::new();
    sessions.retain(|s| {
        if s.0 < now { expired.push(s.clone()); false } else { true }
    });
    expired
}

On Rust 1.87+ the same is sessions.extract_if(.., |s| s.0 < now).collect() without the clone.

Two mutable elements

The checker cannot prove that v[i] and v[j] are different places, so two &mut v[..] conflict (E0499). Use an API that checks or guarantees disjointness.

rust
#[derive(Debug)]
struct Account { balance: i64 }

fn transfer(accts: &mut [Account], from: usize, to: usize, amount: i64) {
    // let a = &mut accts[from]; let b = &mut accts[to];   // E0499
    assert_ne!(from, to, "transfer to self");
    let (a, b) = if from < to {
        let (lo, hi) = accts.split_at_mut(to);
        (&mut lo[from], &mut hi[0])
    } else {
        let (lo, hi) = accts.split_at_mut(from);
        (&mut hi[0], &mut lo[to])
    };
    a.balance -= amount;
    b.balance += amount;
}

Rust 1.86 adds <[T]>::get_disjoint_mut([i, j]), which returns Result<[&mut T; N], GetDisjointMutError> (the error says whether an index was out of bounds or two overlapped), and HashMap::get_disjoint_mut([&k1, &k2]), which returns [Option<&mut V>; N] and panics on duplicate keys. To just exchange two elements, slice.swap(i, j) needs no references at all.

Split borrows of struct fields

A method taking &mut self borrows the whole struct. Inside one function, borrowing two different fields is fine because the checker tracks places (self.a, self.b) separately.

rust
struct Renderer { frames: Vec<String>, stats: Stats }
#[derive(Default)]
struct Stats { drawn: u32 }

impl Stats {
    fn record(&mut self) { self.drawn += 1; }
}

impl Renderer {
    fn draw_all(&mut self) {
        // for f in &self.frames { self.record_frame(f) }   // E0502 if record_frame takes &mut self
        let stats = &mut self.stats;                         // borrow the field, not self
        for frame in &self.frames {
            let _ = frame.len();
            stats.record();
        }
    }
}

Three ways to keep the split when the logic lives in a helper:

  1. Make the helper a method on the field's type (Stats::record above).
  2. Make it an associated function that takes the fields: fn record(stats: &mut Stats, frame: &str).
  3. Split the struct: struct Renderer { frames: Frames, stats: Stats } with methods on each part.

Edition 2021 closures capture disjoint fields too: || self.stats.drawn += 1 captures only self.stats.drawn.

Entry API instead of get-then-insert

rust
use std::collections::HashMap;

fn count_words(text: &str) -> HashMap<&str, usize> {
    let mut counts = HashMap::new();
    for w in text.split_whitespace() {
        *counts.entry(w).or_insert(0) += 1;
    }
    counts
}

fn cache_get<'m>(cache: &'m mut HashMap<u64, Vec<u8>>, key: u64, load: impl FnOnce() -> Vec<u8>) -> &'m mut Vec<u8> {
    // if let Some(v) = cache.get_mut(&key) { return v; }   // E0499 (NLL problem case #3)
    // cache.insert(key, load()); cache.get_mut(&key).unwrap()
    cache.entry(key).or_insert_with(load)
}

entry needs an owned key. With String keys and a &str lookup, the clone on every call can matter; then check first (if cache.contains_key(k) { return cache.get_mut(k).unwrap(); }), which does two lookups on a hit but allocates only on a miss. hashbrown's entry_ref avoids both costs.

Taking values out of &mut

You cannot move a field out of &mut self (E0507) because the struct would be left with a hole. Put something back in the same step:

Field typeTake it withLeaves behind
Option<T>self.x.take()None
Option<T>, conditionally`self.x.take_if(v
T: Default (Vec, String, maps)std::mem::take(&mut self.x)T::default()
any Tstd::mem::replace(&mut self.x, new)new
two placesstd::mem::swap(&mut a, &mut b)each other's values
rust
use std::mem;

struct Batcher { pending: Vec<String>, last_error: Option<String> }

impl Batcher {
    fn flush(&mut self) -> Vec<String> { mem::take(&mut self.pending) }
    fn error(&mut self) -> Option<String> { self.last_error.take() }
}

State machines with mem::replace

Transitions that consume the old state need ownership of it. Swap in a cheap placeholder, match on the old value, store the new one.

rust
use std::mem;

enum Conn {
    Idle,
    Connecting { attempts: u32 },
    Open { session: String },
    Closed,
}

struct Client { state: Conn }

impl Client {
    fn on_connected(&mut self, session: String) {
        self.state = match mem::replace(&mut self.state, Conn::Closed) {
            Conn::Connecting { .. } => Conn::Open { session },
            other => other, // unexpected event: keep the old state
        };
    }
    fn retry(&mut self) {
        if let Conn::Connecting { attempts } = &mut self.state {
            *attempts += 1;                  // in-place change: a `&mut` binding is enough
        } else if matches!(self.state, Conn::Idle) {
            self.state = Conn::Connecting { attempts: 1 };
        }
    }
}

If the method can consume self (fn connect(self) -> Client), typestate types avoid the placeholder.

Indices and keys instead of references

A stored reference freezes its target for as long as the holder lives. Storing an index or key instead lets you mutate the collection freely and re-borrow briefly when needed.

rust
struct Library { books: Vec<Book>, loans: Vec<Loan> }
struct Book { title: String, available: bool }
struct Loan { book: usize, member: String } // index into `books`, not &Book

impl Library {
    fn lend(&mut self, book: usize, member: &str) -> Result<(), String> {
        let b = self.books.get_mut(book).ok_or("no such book")?;
        if !b.available { return Err(format!("{} is out", b.title)); }
        b.available = false;
        self.loans.push(Loan { book, member: member.to_owned() });
        Ok(())
    }
}

Costs: an index can go stale if elements are removed. Use generational indices (slotmap, thunderdome) or never-removing arenas when that matters.

Arenas for graphs and trees

Doubly linked lists, graphs with cycles and parent pointers cannot be expressed with plain &mut. Keep all nodes in a Vec and link them by index.

rust
#[derive(Debug)]
struct Node { name: String, parent: Option<usize>, children: Vec<usize> }

#[derive(Default)]
struct Tree { nodes: Vec<Node> }

impl Tree {
    fn add(&mut self, name: &str, parent: Option<usize>) -> usize {
        let id = self.nodes.len();
        self.nodes.push(Node { name: name.into(), parent, children: Vec::new() });
        if let Some(p) = parent {
            self.nodes[p].children.push(id);
        }
        id
    }
    fn path(&self, mut id: usize) -> Vec<&str> {
        let mut out = vec![self.nodes[id].name.as_str()];
        while let Some(p) = self.nodes[id].parent {
            out.push(&self.nodes[p].name);
            id = p;
        }
        out.reverse();
        out
    }
}

Rc<RefCell<Node>> with Weak parents also works but adds runtime borrow checks, reference cycles to manage and noisy code; prefer the arena unless nodes must be shared across independent owners.

Self-referential structs

struct Doc { text: String, words: Vec<&str> } cannot be written: the references would point into a field of the same struct, and moving the struct would move text's String header (the heap buffer stays, but the type system cannot express that). Options, best first:

  1. Store ranges: words: Vec<std::ops::Range<usize>> and slice on demand (&self.text[r.clone()]).
  2. Split owner and view: keep text: String in the caller and build struct Words<'a> { words: Vec<&'a str> } that borrows it.
  3. Own the pieces: Vec<String> (allocations) or Arc<str> slices.
  4. Crates designed for it: self_cell (small, no proc macro), yoke (zero-copy deserialization), ouroboros (macro-based). Avoid hand-written unsafe pinning.
rust
struct Doc { text: String, words: Vec<std::ops::Range<usize>> }

impl Doc {
    fn new(text: String) -> Self {
        let mut words = Vec::new();
        let mut start = None;
        for (i, ch) in text.char_indices() {
            match (ch.is_whitespace(), start) {
                (false, None) => start = Some(i),
                (true, Some(s)) => { words.push(s..i); start = None; }
                _ => {}
            }
        }
        if let Some(s) = start { words.push(s..text.len()); }
        Doc { text, words }
    }
    fn word(&self, n: usize) -> Option<&str> { self.words.get(n).map(|r| &self.text[r.clone()]) }
}

Worklists instead of recursion over &mut

Recursing while holding &mut to a parent and a child at the same time fails; a queue of indices does not.

rust
use std::collections::VecDeque;

fn mark_reachable(edges: &[Vec<usize>], start: usize) -> Vec<bool> {
    let mut seen = vec![false; edges.len()];
    let mut queue = VecDeque::from([start]);
    while let Some(n) = queue.pop_front() {
        if std::mem::replace(&mut seen[n], true) { continue; }
        queue.extend(edges[n].iter().copied().filter(|&m| !seen[m]));
    }
    seen
}

Choosing a pattern

SituationPattern
Reference used once more after a mutationshorten the borrow / copy out
Loop adds or removes elements of the collection it walkscollect then apply; retain/extract_if
Two elements of one slice/map mutablysplit_at_mut, get_disjoint_mut, swap
Helper method needs &mut self while a field is borrowedsplit borrows, associated fn, sub-structs
Lookup-or-insert returning a referenceentry API
Moving a field out of &mut selftake/mem::take/mem::replace
Enum state transitionsmem::replace with a placeholder, or typestate
Long-lived references between objectsindices/keys, arenas, generational slots
A struct that wants to borrow from itselfranges, owner + view split, self_cell/yoke
More in Rust

rust-cargo-workspace-designer

Design, review and fix Cargo workspaces: when to split a crate (compile time, boundaries, publishing, proc-macros), crates/* vs apps/ + crates/ layouts, virtual manifest vs root package, members/exclude/default-members, resolver 2 vs 3 (MSRV-aware, edition 2024), [workspace.package] and [workspace.dependencies] inheritance, [workspace.lints], root-only profiles (lto, codegen-units), additive features with dep: and weak ?/ syntax, feature unification pitfalls, internal path + version deps, publish = false, Cargo.lock and MSRV policy, xtask, CI with --locked, nextest, rust-cache, sccache, cargo-hack/deny/machete/udeps/semver-checks, and cargo build --timings. Ships a workspace auditor and crate-graph tool.

rust-error-handling-thiserror-anyhow-designer

Design and review Rust error handling with thiserror 2 and anyhow 1: library vs application split, typed error enums with #[from], #[source], #[error(transparent)] and #[non_exhaustive], ErrorKind patterns, context via .context()/.with_context(), bail!/ensure!, source() chains, {:#} and {:?} reports, RUST_BACKTRACE/RUST_LIB_BACKTRACE, Box<dyn Error + Send + Sync>, main -> anyhow::Result or ExitCode, unwrap/expect/panic policy, Option to Result, collecting iterators of Results, axum IntoResponse mapping, retryable errors, tracing, async Send bounds, testing with matches! and downcast_ref, no_std core::error::Error, and snafu, eyre/color-eyre, miette, error-stack.

rust-lifetime-annotation-advisor

Decide when Rust lifetime annotations are needed and which ones: the three elision rules, structs holding references (struct Parser<'a>), owned vs borrowed (String, &str, Cow<'a, str>, Arc<str>), one vs several lifetimes, 'a: 'b bounds, T: 'static misconceptions, Box<dyn Trait + 'a> default object lifetimes, impl Trait + '_ and edition 2024 capture rules with use<..>, variance and PhantomData, HRTB for<'a> Fn(&'a T), GATs and lending iterators, async fn and tokio::spawn 'static, self-referential structs (ouroboros, self_cell, indices, arenas), Rc/Arc to escape lifetime soup. Ships std-only Rust checkers for redundant, missing, unused and over-restrictive lifetimes.

rust-tokio-async-service-builder

Build and review production async Rust services on Tokio 1.x and axum 0.8: runtime choice (#[tokio::main] multi_thread vs current_thread, Builder), axum routing with /{id} paths, State, extractors, IntoResponse error types, axum::serve with_graceful_shutdown, tower/tower-http middleware (TraceLayer, TimeoutLayer, CorsLayer, ConcurrencyLimitLayer, RequestBodyLimitLayer), Arc<AppState>, std vs tokio Mutex, JoinSet, TaskTracker, CancellationToken, select! cancel safety, bounded mpsc, oneshot, broadcast, watch, actors, Semaphore, timeouts, retries, spawn_blocking, sqlx PgPool, reqwest Client reuse, tracing, tests with start_paused and oneshot, Dockerfile. Fixes 'future cannot be sent between threads safely', E0373, Rc or MutexGuard held across await, 'Cannot start a runtime from within a runtime', 'Path segments must not start with :'. Ships std-only Rust scanners and a compiling axum 0.8 scaffold.

rust-trait-design-reviewer

Review and design Rust traits and generic APIs: generics vs impl Trait vs dyn Trait (static vs dynamic dispatch), dyn compatibility / object safety (generic methods, Self returns, where Self: Sized, error[E0038] not dyn compatible / cannot be made into an object), associated types vs generic parameters, blanket impls, coherence and orphan rule (E0117, E0119, E0210), newtype, extension, sealed and marker traits, supertraits and trait upcasting (1.86), async fn in traits (1.75) and the Send bound problem (trait_variant, async-trait), RPITIT, GATs, From/Into/TryFrom/AsRef/Borrow, std trait checklist (Debug, Clone, PartialEq, Hash, Default, Display, Error, Send/Sync, serde feature), #[must_use], #[non_exhaustive], semver hazards, Deref abuse, builders, typestate, PhantomData, Box<dyn Error + Send + Sync>, Fn traits, mockall.

rust-unsafe-block-auditor

Audit unsafe Rust for soundness: inventory every unsafe block, unsafe fn, unsafe impl/trait, extern block and unsafe attribute, check // SAFETY: comments and # Safety docs, and classify each as sound, needs-justification or likely UB, proposing safe alternatives first. Covers the five unsafe superpowers, aliasing (Stacked/Tree Borrows), invalid values, MaybeUninit vs mem::zeroed/uninitialized, transmute, slice::from_raw_parts, Vec::set_len/from_raw_parts, get_unchecked, CString::as_ptr dangling, unsafe impl Send/Sync, Pin projection, FFI (extern "C", repr(C), C-unwind, Box::into_raw/from_raw), strict provenance, static mut and &raw, edition 2024 (unsafe extern, #[unsafe(no_mangle)], unsafe_op_in_unsafe_fn, env::set_var), Miri, sanitizers, cargo-fuzz, loom, cargo-geiger and clippy safety lints.