| name | rust |
| description | Rust coding best practices for idiomatic, efficient, and maintainable code. Use when writing Rust code, reviewing code, or learning Rust patterns. |
| allowed-tools | Read, Edit, Write, Bash, Grep, Glob, Task |
Rust Best Practices
Guidelines for writing idiomatic, efficient, and maintainable Rust code.
Core Principles
- Leverage the type system - Make invalid states unrepresentable
- Prefer compile-time checks - Catch errors before runtime
- Be explicit about ownership - Don't fight the borrow checker
- Write code that passes fmt/clippy first - Not after fixing
Code health
- Prefer smaller files, refactor large files into multi-file modules
- Refactor large modules into several
- Prefer re-usable functions. Don't create separate functions if re-use is not planned.
Error Handling
Use thiserror
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Failed to read config: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse config: {0}")]
Parse(#[from] toml::de::Error),
#[error("Invalid configuration: {message}")]
Invalid { message: String },
}
Never Use .unwrap()
let value = map.get("key").unwrap();
let value = map.get("key").ok_or_else(|| Error::MissingKey("key"))?;
let value = map.get("key").expect("key always present after init");
Ownership & Borrowing
Prefer Borrowing Over Cloning
fn process(data: String) { ... }
process(my_string.clone());
fn process(data: &str) { ... }
process(&my_string);
Use Cow for Flexible Ownership
use std::borrow::Cow;
fn process(data: Cow<'_, str>) -> Cow<'_, str> {
if data.contains("bad") {
Cow::Owned(data.replace("bad", "good"))
} else {
data
}
}
Return Owned Data from Constructors
impl User {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
}
API Design
Builder Pattern for Complex Configuration
#[derive(Default)]
pub struct ServerBuilder {
host: Option<String>,
port: Option<u16>,
timeout: Option<Duration>,
}
impl ServerBuilder {
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self
}
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub fn build(self) -> Result<Server, ConfigError> {
Ok(Server {
host: self.host.unwrap_or_else(|| "localhost".into()),
port: self.port.ok_or(ConfigError::MissingPort)?,
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
})
}
}
Newtype Pattern for Type Safety
fn transfer(from: i64, to: i64, amount: i64) { ... }
pub struct AccountId(i64);
pub struct Amount(i64);
fn transfer(from: AccountId, to: AccountId, amount: Amount) { ... }
Use #[must_use] for Important Returns
#[must_use]
pub fn validate(&self) -> Result<(), ValidationError> {
}
Collections & Iterators
Prefer Iterators Over Loops
let mut results = Vec::new();
for item in items {
if item.is_valid() {
results.push(item.transform());
}
}
let results: Vec<_> = items
.into_iter()
.filter(|item| item.is_valid())
.map(|item| item.transform())
.collect();
Use collect() Type Inference
let vec: Vec<_> = iter.collect();
let map: HashMap<_, _> = iter.collect();
let results: Result<Vec<_>, _> = iter.collect();
Async Patterns
Use tokio for Async Runtime
#[tokio::main]
async fn main() -> Result<()> {
let result = fetch_data().await?;
Ok(())
}
Avoid Blocking in Async Code
async fn bad() {
std::thread::sleep(Duration::from_secs(1));
}
async fn good() {
tokio::time::sleep(Duration::from_secs(1)).await;
}
async fn compute() -> i32 {
tokio::task::spawn_blocking(|| expensive_computation()).await.unwrap()
}
Testing
Unit Tests in Same File
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_edge_case() {
assert!(validate("").is_err());
}
}
Integration Tests in tests/
use my_crate::public_api;
#[test]
fn test_full_workflow() {
let result = public_api::process("input");
assert!(result.is_ok());
}
Use assert! Macros Effectively
assert!(condition);
assert_eq!(left, right);
assert_ne!(left, right);
assert!(result.is_ok());
assert!(result.is_err());
assert_matches!(value, Pattern::Variant { .. });
Performance
Avoid Premature Allocation
fn maybe_string() -> String {
String::from("default")
}
fn maybe_string() -> &'static str {
"default"
}
Use Vec::with_capacity for Known Sizes
let mut vec = Vec::new();
for i in 0..1000 {
vec.push(i);
}
let mut vec = Vec::with_capacity(1000);
for i in 0..1000 {
vec.push(i);
}
Import instead of using absolute paths
let v = tokio::net::TcpStream::connect("localhost:8080");
use tokio::net::TcpStream;
let v = TcpStream::connect("localhost:8080");
Profile Before Optimizing
cargo build --release
cargo flamegraph
Module Organization
Keep Modules Focused
pub mod config;
pub mod client;
pub mod error;
pub use config::Config;
pub use client::Client;
pub use error::Error;
Use pub(crate) for Internal APIs
pub(crate) fn internal_helper() { ... }
Documentation
Document Public APIs
pub fn new(config: Config) -> Result<Self> {
}
Anti-Patterns to Avoid
| Anti-Pattern | Better Approach |
|---|
.unwrap() everywhere | Use ? operator |
clone() to satisfy borrow checker | Restructure ownership |
String parameters | Use &str or impl Into<String> |
| Boolean parameters | Use enums |
| Long function bodies | Extract to smaller functions |
| Deep nesting | Use early returns |
| Magic numbers | Use named constants |
Quick Reference
cargo fmt -- --check && cargo clippy -- -D warnings && cargo test
cargo check
cargo build
cargo build --release
cargo nextest run
cargo doc --open
cargo clippy --fix