Rust Conventions
Ownership & Borrowing
- Prefer borrowing (
&T, &mut T) over cloning — clone only when necessary; unnecessary clones waste allocations
- Use
String for owned data, &str for borrowed string slices — choosing correctly avoids unnecessary allocations
- Move values into functions when the caller no longer needs them
- Use
Cow<str> when a function may or may not need to allocate
Async
- Use
async fn in traits directly — no need for the async-trait crate since Rust 1.75; remove it when upgrading
- Use Tokio as the async runtime for production services — prefer the multi-threaded scheduler unless single-threaded is explicitly required
- Never block inside async code — use
tokio::task::spawn_blocking for CPU-bound or synchronous I/O work
- Set timeouts on all async operations with
tokio::time::timeout — unbounded futures cause silent hangs
Error Handling
- Return
Result<T, E> for all fallible operations — no panics in library code
- Use the
? operator to propagate errors concisely
- Use
thiserror for custom error types in libraries
- Use
anyhow for application-level error handling with context
- Use
color-eyre for CLI applications — provides colorized backtraces and user-facing suggestions
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("file not found: {path}")]
NotFound { path: String },
#[error("parse error at line {line}: {message}")]
Parse { line: usize, message: String },
#[error(transparent)]
Io(#[from] std::io::Error),
}
fn load_config(path: &str) -> Result<Config, ConfigError> {
let content = std::fs::read_to_string(path)?;
parse_config(&content)
}
Traits & Abstractions
- Use traits for abstraction — define behavior, not data
- Keep trait definitions small and focused — one concern per trait
- Implement standard traits:
Debug, Clone, Display where appropriate
- Use derive macros for boilerplate:
#[derive(Debug, Clone, Serialize)]
Lifetimes
- Let the compiler infer lifetimes when possible — annotate only when required
- Name lifetimes descriptively for complex signatures:
'input, 'conn
- Prefer owned types in public APIs to avoid lifetime complexity for callers
- Use
'static only for truly static data — not as a workaround; abusing 'static hides design problems
Dependency Auditing
- Run
cargo audit in CI to check dependencies against the RustSec Advisory Database
- Use
cargo deny for policy enforcement — checks licenses, duplicates, banned crates, and advisories in one pass
- Always commit
Cargo.lock to version control — without it, audit tools cannot determine exact vulnerable versions
Workspaces
- Use Cargo workspaces for projects with multiple crates — share dependency versions and build artifacts
- Define shared dependencies in
[workspace.dependencies] and inherit with { workspace = true } in member crates — prevents version drift
- Split domain logic from infrastructure into separate crates — enforces dependency boundaries at compile time
- Run
cargo fmt on every save — non-negotiable
- Run
clippy and fix all warnings — treat them as errors in CI; clippy catches common mistakes and idiomatic issues
- Use
#[must_use] on functions whose return values must not be ignored
- Enable
#![deny(clippy::all)] in library crates
MSRV (Minimum Supported Rust Version)
- Set
rust-version in Cargo.toml for library crates — communicates the minimum toolchain version to users
- Test against the declared MSRV in CI — a broken MSRV promise is worse than no promise
Testing
- Place unit tests in
#[cfg(test)] modules within the same file
- Use integration tests in the
tests/ directory for public API testing
- Name tests as behaviors:
test_parse_returns_error_for_empty_input
- Use
assert_eq! with descriptive messages for clear failure output
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_returns_error_for_empty_input() {
let result = parse_config("");
assert!(result.is_err(), "empty input should produce an error");
}
#[test]
fn parse_extracts_port_from_valid_config() {
let config = parse_config("port = 8080").unwrap();
assert_eq!(config.port, 8080);
}
}
- Prefer iterators over manual index loops — they optimize better and eliminate bounds-check overhead
- Use
Vec::with_capacity when the size is known ahead of time — avoids repeated reallocations
- Avoid
unsafe unless absolutely necessary — document every usage; unsafe blocks void the compiler’s safety guarantees
- Use
Arc<T> for shared ownership across threads, Rc<T> for single-threaded