Rust Patterns
The borrow checker is a collaborator, not an obstacle. When it fights you, reconsider the design.
Ownership and Borrowing
- Prefer passing by reference (
&T,&mut T) over cloning unless the callee needs ownership. Cloning to satisfy the borrow checker is usually a design smell. - Return owned values from constructors and factory functions; accept references in methods that only inspect.
- When multiple parts of a struct need to be borrowed mutably at the same time, split the struct or introduce a helper method that borrows them together in one
&mut selfcall. - Avoid
Rc<RefCell<T>>in hot paths — it trades compile-time safety for runtime panics and adds indirection. Restructure data to have a single owner instead.
Lifetimes
- Elide lifetimes where the compiler can infer them (one input reference, or
&self/&mut self). Add explicit annotations only when the compiler requires them or when they document an important relationship. - When a struct holds a reference, the lifetime annotation on the struct expresses "this struct cannot outlive the data it borrows." Use
Arc<T>or owned data instead when that constraint is too restrictive. - Avoid
'staticbounds unless you genuinely need the data to live forever (thread spawning, global caches).'staticin trait bounds often signals an architecture that should useArcinstead.
Error Handling
- Use
thiserrorfor library crates (derivesstd::error::Errorwith clean display) andanyhowfor application crates (adds context with.context("while doing X")). - Never use
.unwrap()or.expect()in library code — callers cannot handle a panic. In application code,.expect("invariant: X is always set at startup")is acceptable at program boundaries. - Use the
?operator uniformly. A function that mixes?andmatchfor error handling is inconsistent — pick one style per function. - Represent domain errors as enums with variants, not strings.
AuthError::TokenExpiredis matchable;"token expired"is not.
Trait Design
- Implement standard traits where they make semantic sense:
Displayfor human output,Debugalways,Cloneonly if copying is cheap and meaningful,PartialEq/Eqfor value types. - Keep traits small and focused. A trait with more than 3–5 methods is likely mixing concerns — split it. Blanket implementations become impossible when traits are fat.
- Prefer
impl Traitin function signatures over generic type parameters when the concrete type doesn't need to be named by the caller:fn process(items: impl Iterator<Item = u32>). - Use
Into<T>for constructor arguments that accept multiple types:fn new(name: impl Into<String>). It removes boilerplate at call sites without sacrificing type safety.
Async
- Use
tokioas the default async runtime. Don't mix runtimes in a single binary. - Never block inside an async function: no
std::thread::sleep, no synchronous I/O, noMutex::lockthat might block for more than microseconds. Usetokio::time::sleep, async I/O, andtokio::sync::Mutex. - Use
tokio::spawnfor truly independent tasks; use.awaitfor sequential or dependent work. Over-spawning adds context-switching overhead. select!is for racing multiple futures; don't use it as a workaround for non-cancellable futures — it drops the losing branch, which may leave resources in an inconsistent state.
Unsafe
- Every
unsafeblock requires a// SAFETY:comment explaining why the invariants that make this safe are upheld. - Minimize the surface of unsafe code: wrap it in a safe abstraction immediately. The unsafe impl should be a small, auditable function, not spread across the codebase.
- Run
cargo mirion unsafe code to catch undefined behavior that the compiler cannot detect. - Prefer
unsafein a dedicated module with a module-level safety contract over scatteredunsafeblocks.
Checklist
- No
.unwrap()in library crate paths that callers traverse. - Error types use
thiserror(library) oranyhow(application). - Every
unsafeblock has a// SAFETY:comment. - No blocking calls inside async functions.
- Clippy passes with
cargo clippy -- -D warnings. -
cargo miri testrun on any crate containingunsafe.