Rust Core
Comprehensive guide for writing high-quality, idiomatic, and optimized Rust code. Targets Rust 1.80+ and Edition 2024.
When to Apply
- Writing new Rust functions, structs, or modules
- Implementing error handling or async code
- Designing public APIs for libraries
- Reviewing code for ownership/borrowing issues
- Optimizing memory usage or reducing allocations
- Tuning performance for hot paths
- Refactoring existing Rust code
- Fixing compiler errors (see references/compiler-errors.md)
Edition 2024 Changes (Rust 1.85+)
| Change |
Impact |
Action |
unsafe blocks required for extern blocks |
extern "C" { ... } now needs unsafe { extern "C" { ... } } |
Wrap extern blocks in unsafe |
unsafe_op_in_unsafe_fn lint |
unsafe fn body requires explicit unsafe blocks for unsafe ops |
Add unsafe { ... } inside unsafe fns |
if let temporary scope |
Temporaries dropped at end of if let block |
May extend borrow lifetimes |
gen keyword reserved |
gen { ... } blocks for iterators |
Use gen for lazy iterators |
#[diagnostic::on_unimplemented] |
Better error messages for trait failures |
Add to library traits |
| Nested or-patterns |
Some(1 | 2) stabilized |
Use in match arms |
Run cargo fix --edition to apply most changes automatically.
Rule Categories by Priority
| Priority |
Category |
Prefix |
Rules |
| 1 |
Ownership & Borrowing |
own- |
12 |
| 2 |
Error Handling |
err- |
12 |
| 3 |
Memory Optimization |
mem- |
15 |
| 4 |
API Design |
api- |
15 |
| 5 |
Async/Await |
async- |
15 |
| 6 |
Compiler Optimization |
opt- |
12 |
| 7 |
Naming Conventions |
name- |
16 |
| 8 |
Type Safety |
type- |
10 |
| 9 |
Testing |
test- |
13 |
| 10 |
Documentation |
doc- |
11 |
| 11 |
Performance Patterns |
perf- |
11 |
| 12 |
Project Structure |
proj- |
11 |
| 13 |
Clippy & Linting |
lint- |
11 |
| 14 |
Anti-patterns |
anti- |
15 |
Detailed rules live in rules/ — 14 files, one per prefix group (ownership.md covers own-, error-handling.md covers err-, …). Each ## <rule-id> section inside a category file is one rule, so a grep for a rule id lands directly on it.
Rule Index
Ownership & Borrowing (own-)
| Rule |
Description |
own-borrow-over-clone |
Prefer &T borrowing over .clone() |
own-slice-over-vec |
Accept &[T] not &Vec<T>, &str not &String |
own-cow-conditional |
Use Cow<'a, T> for conditional ownership |
own-arc-shared |
Use Arc<T> for thread-safe shared ownership |
own-rc-single-thread |
Use Rc<T> for single-threaded sharing |
own-refcell-interior |
Use RefCell<T> for interior mutability (single-thread) |
own-mutex-interior |
Use Mutex<T> for interior mutability (multi-thread) |
own-rwlock-readers |
Use RwLock<T> when reads dominate writes |
own-copy-small |
Derive Copy for small, trivial types |
own-clone-explicit |
Make Clone explicit, avoid implicit copies |
own-move-large |
Move large data instead of cloning |
own-lifetime-elision |
Rely on lifetime elision when possible |
Error Handling (err-)
| Rule |
Description |
err-thiserror-lib |
Use thiserror for library error types |
err-anyhow-app |
Use anyhow for application error handling |
err-result-over-panic |
Return Result, don't panic on expected errors |
err-context-chain |
Add context with .context() or .with_context() |
err-no-unwrap-prod |
Never use .unwrap() in production code |
err-expect-bugs-only |
Use .expect() only for programming errors |
err-question-mark |
Use ? operator for clean propagation |
err-from-impl |
Use #[from] for automatic error conversion |
err-source-chain |
Use #[source] to chain underlying errors |
err-lowercase-msg |
Error messages: lowercase, no trailing punctuation |
err-doc-errors |
Document errors with # Errors section |
err-custom-type |
Create custom error types, not Box<dyn Error> |
Memory Optimization (mem-)
| Rule |
Description |
mem-with-capacity |
Use with_capacity() when size is known |
mem-smallvec |
Use SmallVec for usually-small collections |
mem-arrayvec |
Use ArrayVec for bounded-size collections |
mem-box-large-variant |
Box large enum variants to reduce type size |
mem-boxed-slice |
Use Box<[T]> instead of Vec<T> when fixed |
mem-thinvec |
Use ThinVec for often-empty vectors |
mem-clone-from |
Use clone_from() to reuse allocations |
mem-reuse-collections |
Reuse collections with clear() in loops |
mem-avoid-format |
Avoid format!() when string literals work |
mem-write-over-format |
Use write!() instead of format!() |
mem-arena-allocator |
Use arena allocators for batch allocations |
mem-zero-copy |
Use zero-copy patterns with slices and Bytes |
mem-compact-string |
Use CompactString for small string optimization |
mem-smaller-integers |
Use smallest integer type that fits |
mem-assert-type-size |
Assert hot type sizes to prevent regressions |
API Design (api-)
| Rule |
Description |
api-builder-pattern |
Use Builder pattern for complex construction |
api-builder-must-use |
Add #[must_use] to builder types |
api-newtype-safety |
Use newtypes for type-safe distinctions |
api-typestate |
Use typestate for compile-time state machines |
api-sealed-trait |
Seal traits to prevent external implementations |
api-extension-trait |
Use extension traits to add methods to foreign types |
api-parse-dont-validate |
Parse into validated types at boundaries |
api-impl-into |
Accept impl Into<T> for flexible string inputs |
api-impl-asref |
Accept impl AsRef<T> for borrowed inputs |
api-must-use |
Add #[must_use] to Result-returning functions |
api-non-exhaustive |
Use #[non_exhaustive] for future-proof enums/structs |
api-from-not-into |
Implement From, not Into (auto-derived) |
api-default-impl |
Implement Default for sensible defaults |
api-common-traits |
Implement Debug, Clone, PartialEq eagerly |
api-serde-optional |
Gate Serialize/Deserialize behind feature flag |
Async/Await (async-)
| Rule |
Description |
async-tokio-runtime |
Use Tokio for production async runtime |
async-no-lock-await |
Never hold Mutex/RwLock across .await |
async-spawn-blocking |
Use spawn_blocking for CPU-intensive work |
async-tokio-fs |
Use tokio::fs not std::fs in async code |
async-cancellation-token |
Use CancellationToken for graceful shutdown |
async-join-parallel |
Use tokio::join! for parallel operations |
async-try-join |
Use tokio::try_join! for fallible parallel ops |
async-select-racing |
Use tokio::select! for racing/timeouts |
async-bounded-channel |
Use bounded channels for backpressure |
async-mpsc-queue |
Use mpsc for work queues |
async-broadcast-pubsub |
Use broadcast for pub/sub patterns |
async-watch-latest |
Use watch for latest-value sharing |
async-oneshot-response |
Use oneshot for request/response |
async-joinset-structured |
Use JoinSet for dynamic task groups |
async-clone-before-await |
Clone data before await, release locks |
Compiler Optimization (opt-)
| Rule |
Description |
opt-inline-small |
Use #[inline] for small hot functions |
opt-inline-always-rare |
Use #[inline(always)] sparingly |
opt-inline-never-cold |
Use #[inline(never)] for cold paths |
opt-cold-unlikely |
Use #[cold] for error/unlikely paths |
opt-likely-hint |
Use likely()/unlikely() for branch hints |
opt-lto-release |
Enable LTO in release builds |
opt-codegen-units |
Use codegen-units = 1 for max optimization |
opt-pgo-profile |
Use PGO for production builds |
opt-target-cpu |
Set target-cpu=native for local builds |
opt-bounds-check |
Use iterators to avoid bounds checks |
opt-simd-portable |
Use portable SIMD for data-parallel ops |
opt-cache-friendly |
Design cache-friendly data layouts (SoA) |
Naming Conventions (name-)
| Rule |
Description |
name-types-camel |
Use UpperCamelCase for types, traits, enums |
name-variants-camel |
Use UpperCamelCase for enum variants |
name-funcs-snake |
Use snake_case for functions, methods, modules |
name-consts-screaming |
Use SCREAMING_SNAKE_CASE for constants/statics |
name-lifetime-short |
Use short lowercase lifetimes: 'a, 'de, 'src |
name-type-param-single |
Use single uppercase for type params: T, E, K, V |
name-as-free |
as_ prefix: free reference conversion |
name-to-expensive |
to_ prefix: expensive conversion |
name-into-ownership |
into_ prefix: ownership transfer |
name-no-get-prefix |
No get_ prefix for simple getters |
name-is-has-bool |
Use is_, has_, can_ for boolean methods |
name-iter-convention |
Use iter/iter_mut/into_iter for iterators |
name-iter-method |
Name iterator methods consistently |
name-iter-type-match |
Iterator type names match method |
name-acronym-word |
Treat acronyms as words: Uuid not UUID |
name-crate-no-rs |
Crate names: no -rs suffix |
Type Safety (type-)
| Rule |
Description |
type-newtype-ids |
Wrap IDs in newtypes: UserId(u64) |
type-newtype-validated |
Newtypes for validated data: Email, Url |
type-enum-states |
Use enums for mutually exclusive states |
type-option-nullable |
Use Option<T> for nullable values |
type-result-fallible |
Use Result<T, E> for fallible operations |
type-phantom-marker |
Use PhantomData<T> for type-level markers |
type-never-diverge |
Use ! type for functions that never return |
type-generic-bounds |
Add trait bounds only where needed |
type-no-stringly |
Avoid stringly-typed APIs, use enums/newtypes |
type-repr-transparent |
Use #[repr(transparent)] for FFI newtypes |
Testing (test-)
| Rule |
Description |
test-cfg-test-module |
Use #[cfg(test)] mod tests { } |
test-use-super |
Use use super::*; in test modules |
test-integration-dir |
Put integration tests in tests/ directory |
test-descriptive-names |
Use descriptive test names |
test-arrange-act-assert |
Structure tests as arrange/act/assert |
test-proptest-properties |
Use proptest for property-based testing |
test-mockall-mocking |
Use mockall for trait mocking |
test-mock-traits |
Use traits for dependencies to enable mocking |
test-fixture-raii |
Use RAII pattern (Drop) for test cleanup |
test-tokio-async |
Use #[tokio::test] for async tests |
test-should-panic |
Use #[should_panic] for panic tests |
test-criterion-bench |
Use criterion for benchmarking |
test-doctest-examples |
Keep doc examples as executable tests |
test-mutation-assertions |
Use mutation testing to verify test assertions |
test-mutation-trustworthiness |
Make mutation runs trustworthy and affordable |
Documentation (doc-)
| Rule |
Description |
doc-all-public |
Document all public items with /// |
doc-module-inner |
Use //! for module-level documentation |
doc-examples-section |
Include # Examples with runnable code |
doc-errors-section |
Include # Errors for fallible functions |
doc-panics-section |
Include # Panics for panicking functions |
doc-safety-section |
Include # Safety for unsafe functions |
doc-question-mark |
Use ? in examples, not .unwrap() |
doc-hidden-setup |
Use # prefix to hide example setup code |
doc-intra-links |
Use intra-doc links: [Vec] |
doc-link-types |
Link related types and functions in docs |
doc-cargo-metadata |
Fill Cargo.toml metadata |
Performance Patterns (perf-)
| Rule |
Description |
perf-iter-over-index |
Prefer iterators over manual indexing |
perf-iter-lazy |
Keep iterators lazy, collect() only when needed |
perf-collect-once |
Don't collect() intermediate iterators |
perf-entry-api |
Use entry() API for map insert-or-update |
perf-drain-reuse |
Use drain() to reuse allocations |
perf-extend-batch |
Use extend() for batch insertions |
perf-chain-avoid |
Avoid chain() in hot loops |
perf-collect-into |
Use collect_into() for reusing containers |
perf-black-box-bench |
Use black_box() in benchmarks |
perf-release-profile |
Optimize release profile settings |
perf-profile-first |
Profile before optimizing |
Project Structure (proj-)
| Rule |
Description |
proj-lib-main-split |
Keep main.rs minimal, logic in lib.rs |
proj-mod-by-feature |
Organize modules by feature, not type |
proj-flat-small |
Keep small projects flat |
proj-mod-rs-dir |
Use mod.rs for multi-file modules |
proj-pub-crate-internal |
Use pub(crate) for internal APIs |
proj-pub-super-parent |
Use pub(super) for parent-only visibility |
proj-pub-use-reexport |
Use pub use for clean public API |
proj-prelude-module |
Create prelude module for common imports |
proj-bin-dir |
Put multiple binaries in src/bin/ |
proj-workspace-large |
Use workspaces for large projects |
proj-workspace-deps |
Use workspace dependency inheritance |
Clippy & Linting (lint-)
| Rule |
Description |
lint-deny-correctness |
#![deny(clippy::correctness)] |
lint-warn-suspicious |
#![warn(clippy::suspicious)] |
lint-warn-style |
#![warn(clippy::style)] |
lint-warn-complexity |
#![warn(clippy::complexity)] |
lint-warn-perf |
#![warn(clippy::perf)] |
lint-pedantic-selective |
Enable clippy::pedantic selectively |
lint-missing-docs |
#![warn(missing_docs)] |
lint-unsafe-doc |
#![warn(clippy::undocumented_unsafe_blocks)] |
lint-cargo-metadata |
#![warn(clippy::cargo)] for published crates |
lint-rustfmt-check |
Run cargo fmt --check in CI |
lint-workspace-lints |
Configure lints at workspace level |
Anti-patterns (anti-)
| Rule |
Description |
anti-unwrap-abuse |
Don't use .unwrap() in production code |
anti-expect-lazy |
Don't use .expect() for recoverable errors |
anti-clone-excessive |
Don't clone when borrowing works |
anti-lock-across-await |
Don't hold locks across .await |
anti-string-for-str |
Don't accept &String when &str works |
anti-vec-for-slice |
Don't accept &Vec<T> when &[T] works |
anti-index-over-iter |
Don't use indexing when iterators work |
anti-panic-expected |
Don't panic on expected/recoverable errors |
anti-empty-catch |
Don't use empty if let Err(_) = ... blocks |
anti-over-abstraction |
Don't over-abstract with excessive generics |
anti-premature-optimize |
Don't optimize before profiling |
anti-type-erasure |
Don't use Box<dyn Trait> when impl Trait works |
anti-format-hot-path |
Don't use format!() in hot paths |
anti-collect-intermediate |
Don't collect() intermediate iterators |
anti-stringly-typed |
Don't use strings for structured data |
Ownership & Borrowing
- Prefer
&T borrowing over .clone()
- Accept
&[T] not &Vec<T>, &str not &String
- Use
Cow<'a, T> for conditional ownership
- Use
Arc<T> for thread-safe shared ownership, Rc<T> for single-threaded
- Use
RefCell<T> (single-thread) or Mutex<T>/RwLock<T> (multi-thread) for interior mutability
- Derive
Copy for small, trivial types
- Move large data instead of cloning
- Rely on lifetime elision when possible
Error Handling
- Use
thiserror for library error types, anyhow for application error handling
- Return
Result, don't panic on expected errors
- Add context with
.context() or .with_context()
- Never use
.unwrap() in production code
- Use
.expect() only for programming errors
- Use
? operator for clean propagation
- Use
#[from] for automatic error conversion
- Use
#[source] to chain underlying errors
- Error messages: lowercase, no trailing punctuation
- Document errors with
# Errors section
- Create custom error types, not
Box<dyn Error>
Memory Optimization
- Use
with_capacity() when size is known
- Use
SmallVec for usually-small collections, ArrayVec for bounded-size
- Box large enum variants to reduce type size
- Use
Box<[T]> instead of Vec<T> when fixed size
- Use
clone_from() to reuse allocations
- Reuse collections with
clear() in loops
- Avoid
format!() when string literals work; use write!() instead
- Use smallest integer type that fits
- Assert hot type sizes to prevent regressions
API Design
- Use Builder pattern for complex construction; add
#[must_use] to builder types
- Use newtypes for type-safe distinctions (
UserId(u64), Email, Url)
- Use typestate for compile-time state machines
- Seal traits to prevent external implementations when appropriate
- Use extension traits to add methods to foreign types
- Parse into validated types at boundaries
- Accept
impl Into<T> for flexible string inputs, impl AsRef<T> for borrowed inputs
- Add
#[must_use] to Result-returning functions
- Use
#[non_exhaustive] for future-proof enums/structs
- Implement
From, not Into (auto-derived)
- Implement
Default for sensible defaults
- Implement
Debug, Clone, PartialEq eagerly
- Gate
Serialize/Deserialize behind feature flag
Async/Await
- Use Tokio for production async runtime
- Never hold
Mutex/RwLock across .await
- Use
spawn_blocking for CPU-intensive work
- Use
tokio::fs not std::fs in async code
- Use
CancellationToken for graceful shutdown
- Use
tokio::join! for parallel operations, tokio::try_join! for fallible
- Use
tokio::select! for racing/timeouts
- Use bounded channels for backpressure
- Use
JoinSet for dynamic task groups
- Clone data before await, release locks
Testing
- Use
#[cfg(test)] mod tests { } with use super::*;
- Put integration tests in
tests/ directory
- Structure tests as arrange/act/assert
- Use
#[tokio::test] for async tests
- Use
proptest for property-based testing
- Use
mockall for trait mocking; design dependencies as traits
- Use RAII pattern (Drop) for test cleanup
- Use
criterion for benchmarking
- Keep doc examples as executable tests
- Use
#[should_panic] for panic tests
- Aim for 80%+ coverage; test edge cases and error paths
Anti-Patterns
The 15 anti-* rules are indexed in the Rule Index above with full rationale and ❌/✅ examples in rules/anti-patterns.md.
Essential Commands
cargo build --release # Build with optimizations
cargo test # Run all tests
cargo test -- --nocapture # Run tests with output
cargo clippy -- -D warnings # Lint with warnings as errors
cargo fmt --check # Check formatting
cargo doc --no-deps # Generate documentation
cargo bench # Run benchmarks
cargo fix --edition # Auto-fix edition migration
Cargo.toml Release Profile
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
strip = true
[profile.dev.package."*"]
opt-level = 3
References
rules/ — 14 category files (179 rules total), one file per prefix group, read on demand
references/compiler-errors.md — Compiler error quick reference (E0382, E0502, E0716, etc.)