Rust Correctness Reference
The language-level discipline behind every Rust skill. Organized as forbidden
patterns with fixes — these are the bugs that compile (or almost compile) and
bite later.
Ownership & Borrowing
Rules: every value has one owner; any number of & XOR exactly one &mut;
references never outlive their referent.
| Forbidden |
Fix |
.clone() to silence a borrow error |
Restructure: shorten the borrow's scope, or split the struct |
& and &mut to the same data alive together (E0502) |
Narrow the shared borrow's lifetime (NLL ends a borrow at last use) |
| Returning a reference to a local (E0515) |
Return the owned value |
| Moving out of a borrowed value (E0507) |
std::mem::take/replace, clone deliberately, or take ownership |
&Vec<T> / &String parameters |
&[T] / &str — accept slices |
Two &mut to the same data (E0499) |
Split borrows per field, or reorder operations |
Panic vs Result
Result for ALL fallible operations — propagate with ?.
panic!/unwrap/expect ONLY in: tests, main() bootstrap, and genuinely
unreachable invariants (with a message saying why).
- NEVER in handlers, use cases, repositories, or library code.
let Some(x) = opt else { return Err(...) } — let else for early returns.
Async Concurrency (Tokio)
| Forbidden |
Why |
Fix |
std::sync::MutexGuard held across .await |
Future becomes !Send — won't spawn |
Drop the guard before awaiting, or tokio::sync::Mutex |
Rc/RefCell in spawned tasks |
!Send |
Arc / Arc<Mutex<T>> |
Blocking calls (std::fs, heavy CPU, reqwest::blocking) in async |
Starves the runtime |
tokio::task::spawn_blocking or async equivalents |
| Inconsistent lock acquisition order |
Deadlock |
One canonical order, documented |
.lock().unwrap() everywhere |
Poisoned mutex panics cascade |
Handle PoisonError or use tokio Mutex (no poisoning) |
DB connection held across unrelated .awaits |
Pool exhaustion |
Acquire late, release early |
Channels (mpsc, broadcast, watch) over shared state where the data flows
one way.
Smart Pointer Selection
| Need |
Use |
| Heap allocation / recursive type |
Box<T> |
| Shared ownership, single thread |
Rc<T> |
| Shared ownership, multi-thread / async |
Arc<T> |
| Interior mutability, single thread |
RefCell<T> (runtime-checked) |
| Interior mutability, multi-thread |
Mutex<T> / RwLock<T> (tokio variants in async) |
| Breaking reference cycles |
Weak<T> |
Arc<Mutex<T>> with no writer is a smell — Arc<T> suffices for read-only.
Lifetimes
- Let elision work; annotate only when the compiler asks.
- A named lifetime ties OUTPUT to INPUT:
fn first<'a>(s: &'a str) -> &'a str.
'static means "lives forever or owned" — don't reach for it to silence
E0597; restructure ownership instead.
Traits & Generics
impl Trait in argument/return position for single concrete types; generics
<T: Trait> when the caller chooses; dyn Trait when you need runtime
dispatch or heterogeneous collections (ports use Arc<dyn Repo>).
- Implement
From, get Into free — never implement both.
- Object safety:
async fn in traits needs #[async_trait] for dyn use.
Pattern Matching
match must be exhaustive — avoid _ => on YOUR OWN enums; adding a variant
should break compilation everywhere it matters.
if let for single-variant interest; let else for extract-or-return.
matches!() for boolean checks.
Iterators & Collections
- Iterator chains over index loops:
filter/map/collect — lazy until consumed.
with_capacity when the size is known.
entry() API for HashMap upsert: map.entry(k).or_insert_with(Vec::new).push(v).
- Closures: default borrow capture;
move for spawned tasks and returns.
Unsafe
Forbidden in application services. If FFI or a sound abstraction genuinely
requires it: isolate in one module, document the safety contract on every
unsafe block, and test the invariants. Never transmute as a shortcut.
Compiler Error Quick-Route
| Error |
Section |
| E0382 (use after move), E0499, E0502, E0505, E0507 |
Ownership & Borrowing |
| E0106, E0515, E0597 (lifetime) |
Lifetimes |
| "future cannot be sent between threads" |
Async Concurrency |
| E0038 (not object safe) |
Traits & Generics |
| non-exhaustive match |
Pattern Matching |
When MULTIPLE compiler errors cascade: fix in dependency order, rebuild ONCE
(see batch-error-resolution).
1---2name: rust-correctness3description: Rust language correctness reference — ownership and borrowing, lifetimes, async concurrency hazards, smart pointer selection, panic-vs-Result philosophy, and exhaustive pattern matching, with detectors for the forbidden patterns behind common compiler errors (E0382, E0499, E0502, E0597) and async bugs. Use when writing any non-trivial Rust, fixing borrow-checker or Send/Sync errors, or reviewing Rust for correctness. Do not use for Python.4---56# Rust Correctness Reference78The language-level discipline behind every Rust skill. Organized as forbidden9patterns with fixes — these are the bugs that compile (or almost compile) and10bite later.1112## Ownership & Borrowing1314Rules: every value has one owner; any number of `&` XOR exactly one `&mut`;15references never outlive their referent.1617| Forbidden | Fix |18|---|---|19| `.clone()` to silence a borrow error | Restructure: shorten the borrow's scope, or split the struct |20| `&` and `&mut` to the same data alive together (E0502) | Narrow the shared borrow's lifetime (NLL ends a borrow at last use) |21| Returning a reference to a local (E0515) | Return the owned value |22| Moving out of a borrowed value (E0507) | `std::mem::take`/`replace`, clone deliberately, or take ownership |23| `&Vec<T>` / `&String` parameters | `&[T]` / `&str` — accept slices |24| Two `&mut` to the same data (E0499) | Split borrows per field, or reorder operations |2526## Panic vs Result2728- `Result` for ALL fallible operations — propagate with `?`.29- `panic!`/`unwrap`/`expect` ONLY in: tests, `main()` bootstrap, and genuinely30 unreachable invariants (with a message saying why).31- NEVER in handlers, use cases, repositories, or library code.32- `let Some(x) = opt else { return Err(...) }` — `let else` for early returns.3334## Async Concurrency (Tokio)3536| Forbidden | Why | Fix |37|---|---|---|38| `std::sync::MutexGuard` held across `.await` | Future becomes `!Send` — won't spawn | Drop the guard before awaiting, or `tokio::sync::Mutex` |39| `Rc`/`RefCell` in spawned tasks | `!Send` | `Arc` / `Arc<Mutex<T>>` |40| Blocking calls (`std::fs`, heavy CPU, `reqwest::blocking`) in async | Starves the runtime | `tokio::task::spawn_blocking` or async equivalents |41| Inconsistent lock acquisition order | Deadlock | One canonical order, documented |42| `.lock().unwrap()` everywhere | Poisoned mutex panics cascade | Handle `PoisonError` or use tokio Mutex (no poisoning) |43| DB connection held across unrelated `.await`s | Pool exhaustion | Acquire late, release early |4445Channels (`mpsc`, `broadcast`, `watch`) over shared state where the data flows46one way.4748## Smart Pointer Selection4950| Need | Use |51|---|---|52| Heap allocation / recursive type | `Box<T>` |53| Shared ownership, single thread | `Rc<T>` |54| Shared ownership, multi-thread / async | `Arc<T>` |55| Interior mutability, single thread | `RefCell<T>` (runtime-checked) |56| Interior mutability, multi-thread | `Mutex<T>` / `RwLock<T>` (tokio variants in async) |57| Breaking reference cycles | `Weak<T>` |5859`Arc<Mutex<T>>` with no writer is a smell — `Arc<T>` suffices for read-only.6061## Lifetimes6263- Let elision work; annotate only when the compiler asks.64- A named lifetime ties OUTPUT to INPUT: `fn first<'a>(s: &'a str) -> &'a str`.65- `'static` means "lives forever or owned" — don't reach for it to silence66 E0597; restructure ownership instead.6768## Traits & Generics6970- `impl Trait` in argument/return position for single concrete types; generics71 `<T: Trait>` when the caller chooses; `dyn Trait` when you need runtime72 dispatch or heterogeneous collections (ports use `Arc<dyn Repo>`).73- Implement `From`, get `Into` free — never implement both.74- Object safety: `async fn` in traits needs `#[async_trait]` for `dyn` use.7576## Pattern Matching7778- `match` must be exhaustive — avoid `_ =>` on YOUR OWN enums; adding a variant79 should break compilation everywhere it matters.80- `if let` for single-variant interest; `let else` for extract-or-return.81- `matches!()` for boolean checks.8283## Iterators & Collections8485- Iterator chains over index loops: `filter`/`map`/`collect` — lazy until consumed.86- `with_capacity` when the size is known.87- `entry()` API for HashMap upsert: `map.entry(k).or_insert_with(Vec::new).push(v)`.88- Closures: default borrow capture; `move` for spawned tasks and returns.8990## Unsafe9192Forbidden in application services. If FFI or a sound abstraction genuinely93requires it: isolate in one module, document the safety contract on every94`unsafe` block, and test the invariants. Never `transmute` as a shortcut.9596## Compiler Error Quick-Route9798| Error | Section |99|---|---|100| E0382 (use after move), E0499, E0502, E0505, E0507 | Ownership & Borrowing |101| E0106, E0515, E0597 (lifetime) | Lifetimes |102| "future cannot be sent between threads" | Async Concurrency |103| E0038 (not object safe) | Traits & Generics |104| non-exhaustive match | Pattern Matching |105106When MULTIPLE compiler errors cascade: fix in dependency order, rebuild ONCE107(see batch-error-resolution).