name: rust description: > Mental-model reset for Rust. Use when writing or reviewing Rust code to shift from "it compiles" to "thinks in Rust." Triggers on Rust code review, "is this idiomatic", borrow-checker errors, API design, domain modeling, ownership, lifetimes, errors, traits, async/Tokio, unsafe, serde, FFI, tests, performance, Cargo structure, .rs files, Cargo.toml, rustc diagnostics, clippy findings, Result/Option, thiserror vs anyhow, newtype, typestate, enum vs trait, dyn Trait, Send/Sync, Pin, Miri, PyO3, napi-rs, cxx, UniFFI, wasm-bindgen, serde attributes, or feature unification.
Thinking in Rust
You already know Rust syntax. Change the defaults you reach for first when modeling a domain, handling ownership, designing APIs, or crossing boundaries.
The core failure mode: writing Rust that compiles but thinks like Python, Java, TypeScript, or C. Bare String for domain types. bool for states. Trait objects for closed sets. Error(String) for everything. _ => in every match. Index loops. Sentinel values. Getters and setters on every field. clone() to quiet the compiler. unsafe to escape design pressure. These compile. They are wrong.
Most of these habits come from languages without sum types, ownership, zero-cost newtypes, or exhaustive matching. Recognizing where a pattern comes from helps you see why it is wrong in Rust.
When reviewing Rust, start with the shape of the program: what invariants are represented, who owns each value, which states are impossible, where errors cross boundaries, and whether any escape hatch is hiding a design problem.
Treat these as strong defaults, not rigid laws: when unsure, choose the approach that moves invariants into types and lets the compiler enforce them.
How Rust Thinks
Model the domain in types
- Every string with domain meaning is a newtype. Bare
Stringerases domain knowledge. The compiler cannot distinguish an email from a username from a URL. Wrap it, validate at construction, keep the field private. See references/newtypes-and-domain-types.md. - Every boolean parameter is a lie — use an enum.
trueandfalsecarry no meaning at the call site and cannot extend to a third state. Replace flags with named variants. Applies to struct fields too: correlated booleans are a state machine in disguise. See references/bool-to-enum.md. - Every "I don't know" is explicit.
Option<bool>has three states but none of them are named. Empty collections can mean "checked and empty" or "not checked yet." Make each state a named variant. See references/option-bool-to-enum.md. - Every match on your enum is exhaustive — no wildcard
_ =>arms. Wildcards silence the compiler when you add variants. List every variant of enums you control._ =>is for foreign#[non_exhaustive]types and primitives. See references/exhaustive-matching.md. - Every error variant is a domain fact — no
Error(String). String errors throw away structure. Callers cannot match, test, retry, translate, or recover. Libraries expose typed error enums; applications add context. See error-handling.md. - Parse, don't validate. Validation checks data and throws away the proof. Parsing checks data and returns a type that proves the invariant. After parsing, do not re-check downstream. See references/parse-dont-validate.md.
- Enums are the primary modeling tool. Rust enums are sum types. A struct with a
kindfield plusOptionpayload fields is always an enum waiting to be written. See references/enums-as-modeling-tool.md. - Closed sets are enums, not trait objects. If you know all variants at compile time, use an enum: zero-cost dispatch, exhaustive matching, per-variant data. Use generics or
dyn Traitonly when the set is genuinely open. See traits.md. - Boundaries translate; internals model. Serde, FFI, CLI, HTTP, and database edges should convert DTOs into domain types. Do not let wire formats become your internal model. See serde.md and interop.md.
Express ownership and API intent
- Borrow by default — own when intentional. Accept
&str,&[T], and&Pathunless you need to store, mutate, transform, or transfer ownership. See references/borrow-by-default.md. - Function signatures are ownership contracts. A signature should reveal who owns, who borrows, who mutates, and how long values remain valid. If the signature lies, the borrow checker will make you pay. See references/function-signatures.md.
- Clone is not a design tool. Clone for independent ownership, thread transfer, or stored copies. Do not clone because E0382 made you sad. See ownership.md.
- Restructure ownership before
Rc<RefCell<T>>.RefCelltrades compile-time borrow checking for runtime panics. First try split borrows, read-then-write phases, arenas/indices, or explicit ownership flow. See references/ownership-before-refcell.md. - Async is for waiting, not for CPU work. Never block the runtime. Use async I/O,
spawn_blockingfor short blocking calls, and Rayon or dedicated threads for CPU-bound work. See async.md. - Unsafe and atomics require written proofs. Atomics need a small ordering argument. Unsafe needs the smallest safe wrapper,
# Safetydocs,// SAFETY:comments, and Miri when validity or aliasing matters. See atomics.md and unsafe.md.
Express intent in APIs and control flow
- Iterators over index loops.
for i in 0..v.len()risks off-by-one errors and obscures intent. Use.iter(),.enumerate(),.windows(),.zip(), and adapters that say what you mean. See references/iterators-over-indexing.md. Optionover sentinel values.-1,"",0, andu32::MAXas "no value" markers are invisible to the type system. UseOption<T>. See references/option-over-sentinels.md.- One entity, one struct — not parallel collections. Multiple maps or vectors sharing keys depend on discipline, not types. Group related data into a struct and store one collection of that entity. See references/struct-collections.md.
- Transform over mutate. For configuration and construction, prefer consuming
selfchains over&mut selfsetters. Reserve&mut selffor live objects being operated on. See references/transform-over-mutate.md. - Modules are namespaces, not
implblocks. A unit struct with only associated functions is a Java class in disguise. Use modules for free functions; use traits when method syntax matters. See references/impl-namespace.md. - Right-size pattern matching.
matches!()for boolean checks.if letfor one variant.let ... elsefor early return.matchfor real alternatives. Exhaustivematchwhen adding a variant should break the build. See references/pattern-matching-tools.md. - Public fields beat trivial getters. If any value of a field's type is valid, make it
pub. Do not writeget_x()/set_x()that only forwards. When accessors protect invariants, use Rust naming:name(), notget_name(). See references/getter-setter.md. - Visibility is a design tool, not cleanup.
pubis a semver promise. Default to private orpub(crate), group modules by domain, and curate the public facade withpub use. See references/visibility-and-modules.md. - Crate boundaries must earn their names. Stay single-crate until you can name the boundary. Cargo features are additive public capability, not an internal architecture switch. See project-structure.md.
Common Mistakes (Agent Failure Modes)
- Public newtype fields (
pub struct Email(pub String)) → Make the field private; force construction throughparse/newso invariants cannot be bypassed. - Boolean flags leaking into APIs → Replace with enums, even when there are only two states today.
Option<bool>or nestedOptionstate → Name the states. Stop making callers decode truth tables.kindfield plusOptionpayload fields → Replace with an enum carrying per-variant data; delete the impossible states.- Wildcard matches on your own enums → List every variant; adding a variant should break the build.
- Validation that returns
Result<(), E>and then forgets the proof → Parse once at the boundary into a domain type. Error(String)or a crate-wide error blob → Define structured errors for one unit of fallibility.anyhow::Errorin a public library API → Use a library error type; reserveanyhowfor binaries/apps.- Bare
?in application code → Add.context()so the error says what you were doing. - Taking ownership by default → Borrow unless you store, return, transform, or transfer ownership.
clone()as first response to E0382 → Ask who should own the data. Clone only when you can name why.'staticadded to silence a lifetime error → Fix the relationship between lifetimes; do not make your API less useful.&String,&Vec<T>, or&PathBufin APIs → Accept&str,&[T], or&Path.Rc<RefCell<T>>orArc<Mutex<T>>as first resort → Restructure ownership or use message passing.dyn Traitfor a closed set → Use an enum. Interfaces are not free flexibility in Rust.- Blocking or CPU work inside
async fn→ Use async APIs,spawn_blocking, or Rayon/thread pool. - Holding a lock guard across
.await→ Narrow the lock scope or redesign shared state. Ordering::Relaxedbecause it is faster → Write the proof; otherwise use Release/Acquire orSeqCst.- Unsafe to dodge the borrow checker → The pattern is probably wrong. Restructure first.
serde_json::Valueas the internal model → Use DTOs at the boundary and domain types inside.- Benchmarking debug builds or optimizing cold code → Measure
--releasefirst; keep invariants until profiling proves otherwise. - Feature flags for internal workspace architecture → Use modules/crates; features are additive public capability.
Review Checklist
- Domain primitive? → Newtype, enum, or parser-backed type.
- Boolean or
Option<bool>state? → Named enum variants. - Wildcard match on owned enum? → Exhaustive match.
- Validation repeated downstream? → Parse once at the boundary.
- Borrow checker appeased with
clone(),'static,Rc<RefCell<_>>, or unsafe? → Rework ownership first. - Public signature takes owned data but only reads? → Borrow
&str,&[T], or&Path. - Library returns stringly or
anyhowerrors? → Structured public error type. - Polymorphism unclear? → Enum, then generics, then
dynonly for true erasure. - Async code blocks, holds locks across
.await, or fans out unboundedly? → Move blocking work and bound concurrency. - Unsafe or atomics present? → Check the written invariant/proof and run Miri when relevant.
- Serde/FFI/API boundary leaks into internals? → Translate DTOs into domain types.
- Performance concern? → Measure
--releasebefore cleverness. - Everything is
pubor feature-gated internally? → Curate the facade; keep features additive.
Quick Reference
| Code smell | Rust default move | Reference |
|---|---|---|
Bare String/u64 for domain values |
Newtype with private field | references/newtypes-and-domain-types.md |
bool parameter or state field |
Two-variant enum | references/bool-to-enum.md |
Option<bool> / nested Option |
Named enum variants | references/option-bool-to-enum.md |
_ => on your own enum |
List every variant | references/exhaustive-matching.md |
Error(String) in a library |
Typed error enum scoped to the operation | error-handling.md |
| Validate then forget | Parse into a domain type | references/parse-dont-validate.md |
kind field + Option payloads |
Enum with per-variant data | references/enums-as-modeling-tool.md |
Box<dyn Trait> for a closed set |
Enum, or generics if the set is open | traits.md |
Function takes Vec<T>/String but only reads |
Borrow &[T] / &str |
references/borrow-by-default.md |
| Defensive clone or lifetime fight | Redesign ownership before adding escape hatches | ownership.md |
for i in 0..v.len() |
Iterator chain | references/iterators-over-indexing.md |
| Magic number/string for absence | Option<T> |
references/option-over-sentinels.md |
| Parallel collections with shared keys | Single collection of structs | references/struct-collections.md |
&mut self builder setters |
Consuming self chains |
references/transform-over-mutate.md |
| Unit struct with only associated functions | Module with free functions | references/impl-namespace.md |
One meaningful match arm |
if let, let ... else, or matches! |
references/pattern-matching-tools.md |
Trivial get_x() / set_x() |
Public field or x() accessor with invariant |
references/getter-setter.md |
Everything pub / one giant file |
Modules plus curated visibility | references/visibility-and-modules.md |
| Blocking work or unbounded fan-out in async code | Async waits; CPU blocks elsewhere; bound everything | async.md |
| Atomic ordering chosen by vibe | Use atomics only with a written proof | atomics.md |
| Unsafe added to bypass compiler friction | Isolate unsafe and document the invariant | unsafe.md |
| Wire format leaking into internals | Translate DTOs into domain types | serde.md |
| FFI or host-runtime boundary | Keep the ABI small, typed, and panic-safe | interop.md |
| Crate/workspace/API shape unclear | Stay single-crate until the boundary has a name | project-structure.md |
Cross-References
- ownership.md — Borrow checker errors, lifetimes, function signatures, smart pointers,
Cow, clone discipline. - error-handling.md —
thiserrorvsanyhow, structured errors, context, combinators, panic boundaries. - traits.md and type-design.md — Dispatch choices, trait design, newtypes, typestate, builders, phantom types.
- async.md, atomics.md, and unsafe.md — Concurrency, memory ordering, soundness, Miri,
Send/Syncinvariants. - macros.md, testing.md, and performance.md — Generated code, validation strategy, profiling-first optimization.
- serde.md, interop.md, and project-structure.md — Boundaries, DTOs, FFI, workspaces, features, public API surface.