rust-best-practices
Rust coding best practices for idiomatic, efficient, and maintainable code. Use when writing Rust code, reviewing code, or learning Rust patterns.
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
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
Error Handling
Application Code: Use anyhow
use anyhow::{Context, Result, bail};
fn load_config(path: &Path) -> Result<Config> {
let contents = std::fs::read_to_string(path)
.context("Failed to read config file")?;
let config: Config = toml::from_str(&contents)
.context("Failed to parse config")?;
if config.port == 0 {
bail!("Port cannot be 0");
}
Ok(config)
}
Library Code: 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() in Library Code
// BAD
let value = map.get("key").unwrap();
// GOOD
let value = map.get("key").ok_or_else(|| Error::MissingKey("key"))?;
// GOOD (when None is truly impossible)
let value = map.get("key").expect("key always present after init");
Ownership & Borrowing
Prefer Borrowing Over Cloning
// BAD - unnecessary clone
fn process(data: String) { ... }
process(my_string.clone());
// GOOD - borrow when possible
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 // No allocation if unchanged
}
}
Return Owned Data from Constructors
// GOOD - clear ownership
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
// BAD - easy to mix up
fn transfer(from: i64, to: i64, amount: i64) { ... }
// GOOD - compile-time safety
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
// BAD
let mut results = Vec::new();
for item in items {
if item.is_valid() {
results.push(item.transform());
}
}
// GOOD
let results: Vec<_> = items
.into_iter()
.filter(|item| item.is_valid())
.map(|item| item.transform())
.collect();
Use collect() Type Inference
// Collect into Vec
let vec: Vec<_> = iter.collect();
// Collect into HashMap
let map: HashMap<_, _> = iter.collect();
// Collect Results
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
// BAD - blocks the runtime
async fn bad() {
std::thread::sleep(Duration::from_secs(1));
}
// GOOD - async sleep
async fn good() {
tokio::time::sleep(Duration::from_secs(1)).await;
}
// GOOD - spawn blocking for CPU-intensive work
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/
// tests/integration_test.rs
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
// BAD - allocates even if not needed
fn maybe_string() -> String {
String::from("default")
}
// GOOD - return static str when possible
fn maybe_string() -> &'static str {
"default"
}
Use Vec::with_capacity for Known Sizes
// BAD - multiple reallocations
let mut vec = Vec::new();
for i in 0..1000 {
vec.push(i);
}
// GOOD - single allocation
let mut vec = Vec::with_capacity(1000);
for i in 0..1000 {
vec.push(i);
}
Profile Before Optimizing
cargo build --release
cargo flamegraph # requires cargo-flamegraph
Module Organization
Keep Modules Focused
// src/lib.rs
pub mod config;
pub mod client;
pub mod error;
// Re-export public API
pub use config::Config;
pub use client::Client;
pub use error::Error;
Use pub(crate) for Internal APIs
// Public to crate, not external users
pub(crate) fn internal_helper() { ... }
Documentation
Document Public APIs
/// Creates a new client with the given configuration.
///
/// # Arguments
///
/// * `config` - The client configuration
///
/// # Errors
///
/// Returns an error if the configuration is invalid.
///
/// # Examples
///
/// ```
/// let client = Client::new(Config::default())?;
/// ```
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
# Quality gates
cargo fmt -- --check && cargo clippy -- -D warnings && cargo test
# Common cargo commands
cargo check # Fast syntax/type check
cargo build # Debug build
cargo build --release # Release build
cargo test # Run tests
cargo doc --open # Generate and view docs
cargo clippy --fix # Auto-fix lint issues
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-02-04
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!