| name | rust-idioms |
| description | Rust ownership, tokio, thiserror/anyhow, Clippy pedantic, unsafe, lifetimes. |
| paths | ["**/*.rs","**/Cargo.toml"] |
Rust Idioms and Patterns
Core Philosophy
Rust's type system and ownership model are your primary tools for correctness. Lean into the compiler — it is your strongest ally. Write code that is idiomatic, safe, and expressive.
Scope: This file covers Rust-specific coding idioms. For file layout, see references/project-structure.md. For test naming conventions, see testing-strategy.md. For logging library choice, see @.agents/skills/logging-implementation/SKILL.md.
Ownership and Borrowing
-
Prefer borrowing (&T, &mut T) over cloning
- Never
.clone() to silence the borrow checker without a // CLONE: comment explaining why
- Use
Cow<'_, T> when a function may or may not need ownership
- Prefer
&str over String in function parameters, &[T] over Vec<T>
-
Minimize owned data in structs
- Use references with explicit lifetimes when the struct is short-lived
- Use owned types (
String, Vec<T>) when the struct must outlive its inputs
-
Avoid unnecessary Arc<Mutex<T>>
- If data flows one direction, use channels (
tokio::sync::mpsc)
- If data is read-heavy, consider
RwLock over Mutex
- If data is immutable after init, use
Arc<T> without a lock
Error Handling
-
Use the ? operator for propagation — never unwrap() in production code
unwrap() and expect() are acceptable only in:
- Tests (
#[test], #[tokio::test])
- Infallible operations where the invariant is proven (document with
// SAFETY: comment)
- CLI
main() function with clear error messages via expect("reason")
-
Choose error crates by context:
- Library code:
thiserror — define typed error enums
- Application code:
anyhow — ergonomic error chaining
- Never mix: library crates should not depend on
anyhow
-
Error type design:
#[derive(Debug, thiserror::Error)]
pub enum PathfinderError {
#[error("file not found: {path}")]
FileNotFound { path: PathBuf },
#[error("AST parse failed: {0}")]
ParseError(String),
#[error(transparent)]
Io(#[from] std::io::Error),
}
fn do_thing() -> Result<(), String> { ... }
#[must_use]
pub fn create_task(req: CreateTaskRequest) -> Result<Task, TaskError> { ... }
Async and Concurrency
-
Use tokio as the async runtime
- Mark async entry points with
#[tokio::main] or #[tokio::test]
- Prefer
tokio::spawn for concurrent tasks, not std::thread::spawn
- Use
tokio::select! for racing futures, not manual polling
-
Cancellation safety:
- Prefer
tokio::sync::mpsc over tokio::sync::broadcast unless fan-out is needed
- Document cancellation behavior on any
async fn that holds resources across .await
- Use
tokio_util::sync::CancellationToken for graceful shutdown
-
Blocking operations:
- Never call blocking I/O inside async context
- Use
tokio::task::spawn_blocking for CPU-heavy or blocking work
- Use
tokio::fs instead of std::fs inside async functions
Unsafe Code
-
Zero unsafe blocks unless in FFI boundaries
- Tree-sitter C bindings and similar FFI are the only valid use case
- Every
unsafe block must have a // SAFETY: comment explaining the invariant
-
Minimize unsafe surface area:
- Encapsulate
unsafe in a safe wrapper function
- The wrapper's public API must be safe to call from any context
- Write tests that exercise the boundary conditions of
unsafe wrappers
-
Never use unsafe to bypass the borrow checker — restructure the code instead
Lifetimes and Generics
-
Prefer '_ lifetime elision when possible
- Only introduce named lifetimes when the compiler requires them or when they clarify intent
- Use
'a for single lifetime parameters, descriptive names ('input, 'query) for multiple
-
Keep generic bounds simple:
- Prefer concrete types for prototyping, introduce generics when the pattern stabilizes
- Use
impl Trait in argument position for simple cases
- Use
where clauses for complex bounds — never inline complex bounds in <...>
-
Avoid lifetime gymnastics:
- If lifetime annotations become complex, restructure to use owned data or
Arc
- Consider the "split borrow" pattern to avoid borrow checker issues in struct methods
Idiomatic Patterns
-
Builder pattern for types with many optional fields:
- Return
Self from builder methods for chaining
build() returns Result<T, BuildError>, not T
-
Newtype pattern for domain types:
- Wrap primitives:
struct UserId(u64), not bare u64
- Implement
Deref only when the newtype truly "is-a" the inner type
-
Typestate pattern for state machines:
- Different states = different types — invalid transitions are compile errors
- Use this for protocol implementations and lifecycle management
-
From/Into conversions:
- Implement
From<A> for B (never Into directly)
- Use
impl From<X> for Error with thiserror's #[from] attribute
Testing
-
Test organization (Rust-specific — differs from Go/TS):
- Unit tests:
#[cfg(test)] mod tests block at the bottom of each .rs file — this is the idiomatic Rust convention, not a shortcut
- Tests can access private functions via
use super::*
- Code inside
#[cfg(test)] is stripped from production builds
- Do NOT create separate
*_test.rs files — this breaks private access and is non-idiomatic
- Integration tests:
tests/ directory at crate root (each file compiled as a separate crate)
- Only tests public API — use
use my_crate::function;
- No
#[cfg(test)] annotation needed
- Shared helpers:
tests/common/mod.rs (NOT tests/common.rs, which Cargo treats as a test file)
- Use
#[tokio::test] for async tests
-
Test naming: fn test_<function>_<scenario>_<expected>() (snake_case)
-
Assertions:
- Use
assert_eq! / assert_ne! over assert!(a == b) — better error messages
- Use
assert!(matches!(result, Ok(_))) for enum variant checking
-
Property testing: Use proptest or quickcheck for functions with wide input spaces
Clippy and Formatting
-
cargo check for fast iteration during development
cargo check: type-checks without producing a binary — fastest feedback loop
cargo clippy: includes cargo check plus lint rules — use before committing
cargo build: only when you need the actual binary/library artifact
- Never run
cargo build during TDD cycles — it is significantly slower than cargo check
-
cargo clippy must pass with zero warnings before any commit
- Use
#[allow(clippy::...)] only with a // ALLOW: comment explaining why
- Prefer fixing the lint over suppressing it
-
cargo fmt is non-negotiable — all code must be formatted
-
Recommended project-level Clippy configuration (.clippy.toml or Cargo.toml):
[lints.clippy]
pedantic = "warn"
unwrap_used = "deny"
expect_used = "warn"
Dependency Management
- Minimize dependency count — each dependency is an attack surface and compile-time cost
- Pin major versions in
Cargo.toml — use dep = "1" not dep = "*"
- Audit regularly — run
cargo audit to check for known vulnerabilities
- Prefer well-maintained crates — check download count, last commit date, and issue tracker
Related Principles
- Error Handling Principles @error-handling-principles.md
- Security Principles @security-principles.md
- Architectural Patterns — Testability-First Design @architectural-pattern.md
- Concurrency and Threading Principles @concurrency-and-threading-principles.md
- Core Design Principles § Concurrency @core-design-principles.md
- Performance Optimization Principles @performance-optimization-principles.md
- Resource and Memory Management Principles @resources-and-memory-management-principles.md
- Security Mandate @security-mandate.md
- Code Idioms and Conventions @code-idioms-and-conventions.md
- Testing Strategy @testing-strategy.md
- Logging and Observability Mandate @logging-and-observability-mandate.md
- Logging Implementation @.agents/skills/logging-implementation/SKILL.md
- Dependency Management Principles @dependency-management-principles.md