Rust Best Practices
Two authoritative sources of Rust best practices, presented as checklists for quick reference.
How to Use
- Writing/review: scan the relevant checklist sections below
- Detailed guidance: read the reference file linked in each section header
- API design: Rust API Guidelines checklist (C-prefixed); production systems: Microsoft checklist (M-prefixed)
Sources
- Microsoft Pragmatic Rust Guidelines: https://microsoft.github.io/rust-guidelines/
- AI-friendly condensed version: https://microsoft.github.io/rust-guidelines/agents/all.txt
- Rust API Guidelines: https://rust-lang.github.io/api-guidelines/
Technical Standards & Patterns
- Rust Edition: latest stable (2021 or newer)
- Code Style: rustfmt defaults; Linting: clippy with
deny(warnings)in CI - Documentation: one-line
///comment for every public item; expand only when non-obvious - Error Handling:
thiserrortyped error enums (see below) - Dependencies: minimal, prefer std
- Async: tokio; Serialization: serde derive; CLI: clap; Logging:
tracingcrate (see coding-best-practices § Logging Levels) - Testing: cargo test with doc tests; proptest for property-based testing
- Benchmarking: criterion
Error Handling
Preferred crate: thiserror with typed error enums. Always define explicit error enums — never erase error types into opaque wrappers.
Design Principles
Display= user-friendly,Debug= technical:Display(auto-generated by#[error(...)]) is the message shown to users or logged at info level;Debugpreserves the full error chain for diagnostics.- Granular variants over generic strings: add a dedicated variant with
#[source]rather thanformat!-ing into a catch-allGeneric(String)— preserves the error chain, enables structural matching, keepsDisplay/Debugseparation clean. #[from]for automatic conversion: wire upstream errors as#[from]variants so?works without manual mapping.Box<LargeError>for large upstream types keeps the enum size reasonable.- Omit
#[source]when the upstream error is useless (e.g. a channelSendError).
Pattern
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MyError {
#[error("Failed to load configuration")]
Config {
#[source]
source: std::io::Error,
},
#[error("Invalid input: expected {expected}, got {actual}")]
Validation { expected: String, actual: String },
#[error("Database operation failed")]
Database(#[from] rusqlite::Error),
// Box large upstream errors
#[error("SDK call failed")]
Sdk(#[from] Box<SdkError>),
}
Anti-Patterns
Result<T, String>— loses error chain, prevents matching, no#[source].map_err(|e| format!("{e}"))— destroys the original error; use a typed variant- Catch-all
Generic(String)as default — last resort for one-off strings with no upstream error unwrap()/expect()in non-test production code — handle or propagate with?- Exposing raw error strings to end users —
Displayshould be actionable and jargon-free
Common Pitfalls
- Don't clone unnecessarily — use references
- Don't use unwrap() in production code — handle errors properly
- Don't use unsafe without extensive justification and safety comments
- Don't fight the borrow checker — redesign if struggling
- Don't ignore clippy warnings — fix or explicitly allow with reasoning
- Don't use Arc<Mutex> when RefCell or channels would work
- Don't rely on
debug_assert!/debug_assert_eq!/cfg(debug_assertions)for correctness or safety invariants — compiled out in release builds. Validate at runtime and return a typed error.panic!/assert!/.unwrap()are not an acceptable default — reserve for genuinely unrecoverable invariant violations.
Code Quality Tools
- Compilation + Linting:
cargo-cached.sh clippy -p <your crates> --all-targets -- -D warningswhile iterating (nevercargo check— clippy is a strict superset); workspace-wide--all-featuresclippy is the merge gate's job, run once per merged tree. (Wrapper's absolute path is in the SessionStart Rust build environment context — see Build Optimization.) - Formatting:
cargo fmt - Testing:
cargo-cached.sh test -p <your crates>while iterating;cargo test --all-features --workspace(via the wrapper) only at the merge gate. - Security:
cargo audit - Coverage: cargo-tarpaulin or cargo-llvm-cov
- Documentation:
cargo doc --no-deps --open - LSP Diagnostics: rust-analyzer (see LSP Integration below)
Build Optimization
Rust builds are expensive. cargo build, cargo clippy, and cargo test all compile the code — never chain them or run one as a pre-check for another.
- Use LSP as primary feedback loop — rust-analyzer catches errors without a rebuild
- Defer builds to QA phase — don't run
cargo test/cargo clippy/cargo fmtafter every edit - Never use
cargo check—cargo clippyis a strict superset (compilation + lints) - Never pre-compile —
cargo check && cargo testorcargo clippy && cargo buildwastes a full compile cycle; run the target command directly - Capture output with
tee— seecoding-best-practices§ Build & Test Output Capture - Always go through the
cargo-cached.shwrapper for test/clippy/nextest (absolute path announced in the SessionStart Rust build environment context — the plugin-relativescripts/cargo-cached.shonly resolves inside the plugin itself) — identical command + identical tree replays the recorded log instantly, across all agents and worktrees. The PreToolUse hook enforces this for test/clippy/nextest; plainbuildmay route through it for dedup but is not hook-enforced (a build's output is an artifact, not a replayable verdict). - Don't override
CARGO_TARGET_DIR/--target-dirmanually — isolation is automatic: any invocation throughcargo-cached.shauto-derives a per-checkout, path-keyed target dir, so concurrent same-HEAD worktrees/clones can't collide.CLAUDIUS_TARGET_PREFIXroots the hashed dirs elsewhere; an explicitCARGO_TARGET_DIRviaCLAUDIUS_ISOLATE_TARGET=1wins (the manual escape hatch for edge cases — seegrand-admiral§ Worktree Isolation, not routine); unset/empty prefix keeps the canonical default. The hook denies ad-hoc overrides. A rawcargo buildNOT routed through the wrapper uses the machine's shared~/.cargo/config.tomldir and sccache. Caveat: a barecargo metadataoutside the wrapper reports the shared dir, not the isolated one — don't use it to locate a wrapper-built artifact. - Prefer
cargo nextest runfor test-heavy iteration when installed (checkcommand -v cargo-nextest; SessionStart context states availability) — nextest skips doctests, so the merge gate still needs acargo test(or nextest + a separate--docpass).
Code Review Checklist
- Code readability and self-documentation
- DRY compliance: duplicated logic, copy-paste patterns, missing abstractions
- Naming clarity: variables, functions, types, modules
- Error handling completeness (no silent unwrap in non-test code)
- Performance: unnecessary allocations, clone overhead, iterator vs collect patterns
- Test quality: meaningful assertions, edge cases, error paths covered
- Magic numbers replaced with named constants
- Code brevity: flag code that can be expressed in fewer lines without losing clarity
Use RUST-NNN prefix for all findings.
Microsoft Pragmatic Rust Guidelines Checklist
For detailed descriptions of any M-prefixed item, read references/microsoft-guidelines.md.
Universal
- M-PRIOR-ART — Before implementing custom logic, search crates.io and docs.rs for existing well-maintained crates; prefer established crates over custom implementations
- M-UPSTREAM-GUIDELINES — Follow the upstream Rust API Guidelines, Style Guide, and Design Patterns
- M-STATIC-VERIFICATION — Use clippy, rustfmt, cargo-audit, cargo-hack, cargo-udeps, miri
- M-LINT-OVERRIDE-EXPECT — Use
#[expect]instead of#[allow]for lint overrides - M-PUBLIC-DEBUG — All public types implement
Debug - M-PUBLIC-DISPLAY — Public types meant to be read implement
Display - M-SMALLER-CRATES — If in doubt, split the crate into smaller ones
- M-CONCISE-NAMES — Names are free of weasel words (Service, Manager, Factory)
- M-REGULAR-FN — Prefer regular functions over associated functions for non-receiver logic
- M-PANIC-IS-STOP — Panic means "stop the program", never for error communication
- M-PANIC-ON-BUG — Detected programming bugs are panics, not errors
- M-DOCUMENTED-MAGIC — All magic values and behaviors are documented
- M-LOG-STRUCTURED — Use structured logging with message templates
Library / Interoperability
- M-TYPES-SEND — Types are
Sendfor Tokio/runtime compatibility - M-ESCAPE-HATCHES — Native types provide
unsafeescape hatches for FFI - M-DONT-LEAK-TYPES — Don't leak external crate types in public APIs
Library / UX
- M-SIMPLE-ABSTRACTIONS — Abstractions don't visibly nest (no
Foo<Bar<Baz>>) - M-AVOID-WRAPPERS — Avoid smart pointers and wrappers in public APIs
- M-DI-HIERARCHY — Prefer types > generics > dyn traits for dependency injection
- M-ERRORS-CANONICAL-STRUCTS — Errors are canonical structs with backtrace and cause
- M-INIT-BUILDER — Complex type construction uses builders (4+ permutations)
- M-INIT-CASCADED — Complex initialization hierarchies use semantic grouping
- M-SERVICES-CLONE — Service types implement
CloneviaArc<Inner> - M-IMPL-ASREF — Accept
impl AsRef<>where feasible (str, Path, [u8]) - M-IMPL-RANGEBOUNDS — Accept
impl RangeBounds<>where feasible - M-IMPL-IO — Accept
impl Read/impl Writewhere feasible (Sans IO) - M-ESSENTIAL-FN-INHERENT — Essential functionality is inherent, not trait-only
Library / Resilience
- M-MOCKABLE-SYSCALLS — I/O and system calls are mockable
- M-TEST-UTIL — Test utilities are feature-gated behind
test-util - M-STRONG-TYPES — Use the strongest type available (PathBuf over String)
- M-NO-GLOB-REEXPORTS — Don't glob re-export items
- M-AVOID-STATICS — Avoid statics when consistency matters for correctness
Library / Building
- M-OOBE — Libraries work out of the box on all Tier 1 platforms
- M-SYS-CRATES — Native
-syscrates compile without external dependencies - M-FEATURES-ADDITIVE — Features are additive; any combination works
Applications
- M-MIMALLOC-APP — Use mimalloc as global allocator for applications
- M-APP-ERROR — Applications use
thiserrorwith typed error enums
FFI
- M-ISOLATE-DLL-STATE — Isolate DLL state between FFI libraries; share only
#[repr(C)]data
Safety
- M-UNSAFE — Unsafe needs a documented reason and should be avoided
- M-UNSAFE-IMPLIES-UB — Mark functions
unsafeonly when misuse causes UB - M-UNSOUND — All code must be sound; no exceptions
Performance
- M-THROUGHPUT — Optimize for throughput (items per CPU cycle), avoid empty cycles
- M-HOTPATH — Identify, profile, and optimize the hot path early
- M-YIELD-POINTS — Long-running tasks have yield points (10-100us between yields)
Documentation
- M-NO-TOMBSTONES — Never add comments explaining removed code; git history is the record
- M-FIRST-DOC-SENTENCE — First doc sentence is one line, ~15 words
- M-MODULE-DOCS — Non-trivial public modules have
//!documentation - M-CANONICAL-DOCS — Complex APIs have canonical doc sections (Examples, Errors, Panics, Safety)
- M-DOC-INLINE — Mark
pub useitems with#[doc(inline)]
AI
- M-DESIGN-FOR-AI — Design with AI use in mind (strong types, thorough docs, testable APIs)
Rust API Guidelines Checklist
For detailed descriptions of any C-prefixed item, read references/api-guidelines.md.
Naming (C-*)
- C-CASE — Casing conforms to RFC 430 (CamelCase types, snake_case functions)
- C-CONV — Conversions follow
as_(free, borrowed),to_(expensive),into_(owned) - C-GETTER — Getters omit
get_prefix; use field name directly - C-ITER — Collection iterators use
iter,iter_mut,into_iter - C-ITER-TY — Iterator type names match producing methods
- C-FEATURE — Feature names are free of placeholder words (no
use-,with-,no-) - C-WORD-ORDER — Names use consistent word order (verb-object-error)
Interoperability (C-*)
- C-COMMON-TRAITS — Types eagerly implement Copy, Clone, Eq, PartialEq, Ord, Hash, Debug, Display, Default
- C-CONV-TRAITS — Conversions use
From,TryFrom,AsRef,AsMut - C-COLLECT — Collections implement
FromIteratorandExtend - C-SERDE — Data structures implement Serde (optionally feature-gated)
- C-SEND-SYNC — Types are
SendandSyncwhere possible - C-GOOD-ERR — Error types implement
Error + Send + Syncwith meaningfulDisplay - C-NUM-FMT — Binary number types provide Hex, Octal, Binary formatting
- C-RW-VALUE — Generic reader/writer functions take
R: ReadandW: Writeby value
Macros (C-*)
- C-EVOCATIVE — Input syntax mirrors the output it produces
- C-MACRO-ATTR — Item macros compose well with attributes
- C-ANYWHERE — Item macros work anywhere items are allowed
- C-MACRO-VIS — Item macros support visibility specifiers
- C-MACRO-TY — Type fragments are flexible (primitives, paths, generics)
Documentation (C-*)
- C-CRATE-DOC — Crate-level docs are thorough and include examples
- C-EXAMPLE — Non-trivial public items have a rustdoc example
- C-QUESTION-MARK — Examples use
?, nottry!, notunwrap - C-FAILURE — Function docs include Error, Panic, and Safety sections
- C-LINK — Prose contains hyperlinks to relevant things
- C-METADATA — Cargo.toml includes all common metadata
- C-RELNOTES — Release notes document all significant changes
- C-HIDDEN — Rustdoc does not show unhelpful implementation details
Predictability (C-*)
- C-SMART-PTR — Smart pointers do not add inherent methods
- C-CONV-SPECIFIC — Conversions live on the most specific type
- C-METHOD — Functions with a clear receiver are methods
- C-NO-OUT — Functions do not take out-parameters
- C-OVERLOAD — Operator overloads are unsurprising
- C-DEREF — Only smart pointers implement
Deref/DerefMut - C-CTOR — Constructors are static, inherent methods (
new())
Flexibility (C-*)
- C-INTERMEDIATE — Functions expose intermediate results to avoid duplicate work
- C-CALLER-CONTROL — Caller decides where to copy and place data
- C-GENERIC — Functions minimize assumptions using generics
- C-OBJECT — Traits are object-safe if useful as trait objects
Type Safety (C-*)
- C-NEWTYPE — Newtypes provide static distinctions (Miles vs Kilometers)
- C-CUSTOM-TYPE — Arguments convey meaning through types, not
boolorOption - C-BITFLAG — Flag sets use
bitflags, not enums - C-BUILDER — Builders enable construction of complex values
Dependability (C-*)
- C-VALIDATE — Functions validate their arguments (prefer static > dynamic)
- C-DTOR-FAIL — Destructors never fail
- C-DTOR-BLOCK — Destructors that may block have alternatives
Debuggability (C-*)
- C-DEBUG — All public types implement
Debug - C-DEBUG-NONEMPTY —
Debugrepresentation is never empty
Future Proofing (C-*)
- C-SEALED — Sealed traits protect against downstream implementations
- C-STRUCT-PRIVATE — Structs have private fields with accessor methods
- C-NEWTYPE-HIDE — Newtypes encapsulate implementation details
- C-STRUCT-BOUNDS — Data structures do not duplicate derived trait bounds
Necessities (C-*)
- C-STABLE — Public dependencies of a stable crate are stable
- C-PERMISSIVE — Crate and dependencies have a permissive license (MIT/Apache-2.0)
rust-analyzer LSP Integration
The rust-analyzer-lsp plugin (from claude-plugins-official) provides LSP code intelligence for Rust files. When available, use it for:
- Diagnostics: compilation errors, warnings, type mismatches without running
cargo build - Navigation: definitions, references, type hierarchies
- Type inspection: hover for inferred types and documentation
LSP diagnostics are a fast feedback complement, not a replacement — comprehensive checks still require cargo clippy and cargo test.