name: rust-best-practices
description: Rust development best practices, patterns, and conventions. Use when writing Rust code, reviewing .rs files, discussing ownership, lifetimes, borrowing, or Cargo configuration. Triggers on mentions of Rust, Cargo, ownership, borrowing, lifetimes, traits, async Rust, tokio.
Rust Best Practices
Ownership and Borrowing
Prefer Borrowing Over Ownership
// Bad - takes ownership unnecessarily
fn print_name(name: String) {
println!("{}", name);
}
// Good - borrows immutably
fn print_name(name: &str) {
println!("{}", name);
}
Use References Appropriately
// Immutable borrow for reading
fn calculate_length(s: &String) -> usize {
s.len()
}
// Mutable borrow for modification
fn push_char(s: &mut String, c: char) {
s.push(c);
}
Error Handling
Use Result and Option
fn find_user(id: u32) -> Option<User> {
users.get(&id).cloned()
}
fn parse_config(path: &str) -> Result<Config, ConfigError> {
let content = fs::read_to_string(path)?;
toml::from_str(&content).map_err(ConfigError::Parse)
}
Custom Error Types
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("not found: {0}")]
NotFound(String),
#[error("validation error: {field}")]
Validation { field: String },
}
Propagate with ?
fn process_file(path: &str) -> Result<Data, Error> {
let content = fs::read_to_string(path)?;
let parsed = serde_json::from_str(&content)?;
let validated = validate(parsed)?;
Ok(validated)
}
Structs and Enums
Builder Pattern
#[derive(Default)]
pub struct ServerBuilder {
port: Option<u16>,
host: Option<String>,
}
impl ServerBuilder {
pub fn port(mut self, port: u16) -> Self {
self.port = Some(port);
self
}
pub fn host(mut self, host: impl Into<String>) -> Self {
self.host = Some(host.into());
self
}
pub fn build(self) -> Result<Server, BuildError> {
Ok(Server {
port: self.port.unwrap_or(8080),
host: self.host.unwrap_or_else(|| "localhost".into()),
})
}
}
Newtype Pattern
pub struct UserId(u64);
pub struct Email(String);
impl Email {
pub fn new(email: String) -> Result<Self, ValidationError> {
if email.contains('@') {
Ok(Self(email))
} else {
Err(ValidationError::InvalidEmail)
}
}
}
Enums for State
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
Error { message: String },
}
Traits
Implement Standard Traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct User {
pub id: u64,
pub name: String,
}
Trait Objects vs Generics
// Static dispatch (monomorphization)
fn process<T: Handler>(handler: T) { ... }
// Dynamic dispatch (trait object)
fn process(handler: &dyn Handler) { ... }
fn process(handler: Box<dyn Handler>) { ... }
Extension Traits
pub trait StringExt {
fn truncate_ellipsis(&self, max_len: usize) -> String;
}
impl StringExt for str {
fn truncate_ellipsis(&self, max_len: usize) -> String {
if self.len() <= max_len {
self.to_string()
} else {
format!("{}...", &self[..max_len - 3])
}
}
}
Lifetimes
Elision Rules
// Lifetimes elided - single input reference
fn first_word(s: &str) -> &str { ... }
// Explicit when multiple inputs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }
Struct Lifetimes
struct Parser<'a> {
input: &'a str,
position: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Self { input, position: 0 }
}
}
Async Rust
Tokio Patterns
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = fetch_data().await?;
Ok(())
}
async fn fetch_data() -> Result<Data, Error> {
let client = reqwest::Client::new();
let response = client.get(URL).send().await?;
response.json().await.map_err(Into::into)
}
Concurrent Operations
use futures::future::join_all;
async fn fetch_all(urls: Vec<String>) -> Vec<Result<Response, Error>> {
let futures: Vec<_> = urls.into_iter()
.map(|url| fetch_one(url))
.collect();
join_all(futures).await
}
Testing
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
#[test]
#[should_panic(expected = "division by zero")]
fn test_divide_by_zero() {
divide(1, 0);
}
}
Integration Tests
// tests/integration_test.rs
use mylib::process;
#[test]
fn test_full_workflow() {
let result = process("input");
assert!(result.is_ok());
}
Performance
Avoid Unnecessary Allocations
// Bad - allocates new String
fn process(s: &str) -> String {
s.to_uppercase()
}
// Good when possible - use Cow
use std::borrow::Cow;
fn process(s: &str) -> Cow<str> {
if s.chars().any(|c| c.is_lowercase()) {
Cow::Owned(s.to_uppercase())
} else {
Cow::Borrowed(s)
}
}
Use Iterators
// Good - lazy, zero-cost
let sum: i32 = numbers.iter()
.filter(|n| **n > 0)
.map(|n| n * 2)
.sum();
Anti-Patterns to Avoid
.unwrap() in production code
- Excessive
.clone() to "fix" borrow checker
unsafe without clear justification
- Ignoring compiler warnings
- Not using
clippy
- Global mutable state (
lazy_static with Mutex)