| name | rust-best-practices |
| description | Use when writing or refactoring Rust for idioms, ownership/borrowing choices, Result-based error handling, performance, and tests/docs. |
Rust Best Practices
Apply these guidelines when writing or reviewing Rust code.
Quick Reference
Borrowing & Ownership
- Prefer
&T over .clone() unless ownership transfer is required
- Use
&str over String, &[T] over Vec<T> in function parameters
- Small
Copy types (≤24 bytes) can be passed by value
- Use
Cow<'_, T> when ownership is ambiguous
Error Handling
- Return
Result<T, E> for fallible operations; avoid panic! in production
- Never use
unwrap()/expect() outside tests
- Use
thiserror for library errors, anyhow for binaries only
- Prefer
? operator over match chains for error propagation
Performance
- Always benchmark with
--release flag
- Run
cargo clippy -- -D clippy::perf for performance hints
- Avoid cloning in loops; use
.iter() instead of .into_iter() for Copy types
- Prefer iterators over manual loops; avoid intermediate
.collect() calls
Linting
Run regularly: cargo clippy --all-targets --all-features --locked -- -D warnings
Key lints to watch:
redundant_clone - unnecessary cloning
large_enum_variant - oversized variants (consider boxing)
needless_collect - premature collection
Use #[expect(clippy::lint)] over #[allow(...)] with justification comment.
Testing
- Name tests descriptively:
process_should_return_error_when_input_empty()
- One assertion per test when possible
- Use doc tests (
///) for public API examples
- Consider
cargo insta for snapshot testing generated output
Generics & Dispatch
- Prefer generics (static dispatch) for performance-critical code
- Use
dyn Trait only when heterogeneous collections are needed
- Box at API boundaries, not internally
Type State Pattern
Encode valid states in the type system to catch invalid operations at compile time:
struct Connection<State> { _state: PhantomData<State> }
struct Disconnected;
struct Connected;
impl Connection<Connected> {
fn send(&self, data: &[u8]) { }
}
Error Handling Example
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("not found: {entity} with id {id}")]
NotFound { entity: &'static str, id: String },
#[error("validation failed: {0}")]
Validation(String),
}
pub fn find_user(id: &str) -> Result<User, AppError> {
let user = db::query_user(id)?;
user.ok_or_else(|| AppError::NotFound {
entity: "user",
id: id.to_string(),
})
}
#[from] on a variant generates a From<sqlx::Error> impl so ? converts automatically. Use anyhow::Error in main or CLI entry points where you want a catch-all with context chaining (context()/with_context()), and thiserror everywhere you expose errors to callers.
Documentation
// comments explain why (safety, workarounds, design rationale)
/// doc comments explain what and how for public APIs
- Every
TODO needs a linked issue: // TODO(#42): ...
- Enable
#![deny(missing_docs)] for libraries