Skip to main content
Kodelyth ECC
Skill

rust-patterns

Idiomatic Rust patterns, ownership, error handling, traits, concurrency, and best practices for building safe, performant applications.

Invoke via:use rust-patterns
Origin:ECC

Rust Development Patterns

Idiomatic Rust patterns and best practices for building safe, performant, and maintainable applications.

When to Use

  • Writing new Rust code
  • Reviewing Rust code
  • Refactoring existing Rust code
  • Designing crate structure and module layout

How It Works

This skill enforces idiomatic Rust conventions across six key areas: ownership and borrowing to prevent data races at compile time, Result/? error propagation with thiserror for libraries and anyhow for applications, enums and exhaustive pattern matching to make illegal states unrepresentable, traits and generics for zero-cost abstraction, safe concurrency via Arc>, channels, and async/await, and minimal pub surfaces organized by domain.

Core Principles

1. Ownership and Borrowing

Rust's ownership system prevents data races and memory bugs at compile time.

// Good: Pass references when you don't need ownership
fn process(data: &[u8]) -> usize {
    data.len()
}

// Good: Take ownership only when you need to store or consume fn store(data: Vec<u8>) -> Record { Record { payload: data } }

// Bad: Cloning unnecessarily to avoid borrow checker fn process_bad(data: &Vec<u8>) -> usize { let cloned = data.clone(); // Wasteful — just borrow cloned.len() }

Use Cow for Flexible Ownership

use std::borrow::Cow;

fn normalize(input: &str) -> Cow<'_, str> { if input.contains(' ') { Cow::Owned(input.replace(' ', "_")) } else { Cow::Borrowed(input) // Zero-cost when no mutation needed } }

Error Handling

Use Result and ? — Never unwrap() in Production

// Good: Propagate errors with context
use anyhow::{Context, Result};

fn load_config(path: &str) -> Result<Config> { let content = std::fs::read_to_string(path) .with_context(|| format!("failed to read config from {path}"))?; let config: Config = toml::from_str(&content) .with_context(|| format!("failed to parse config from {path}"))?; Ok(config) }

// Bad: Panics on error fn load_config_bad(path: &str) -> Config { let content = std::fs::read_to_string(path).unwrap(); // Panics! toml::from_str(&content).unwrap() }

Library Errors with thiserror, Application Errors with anyhow

// Library code: structured, typed errors
use thiserror::Error;

#[derive(Debug, Error)] pub enum StorageError { #[error("record not found: {id}")] NotFound { id: String }, #[error("connection failed")] Connection(#[from] std::io::Error), #[error("invalid data: {0}")] InvalidData(String), }

// Application code: flexible error handling use anyhow::{bail, Result};

fn run() -> Result<()> { let config = load_config("app.toml")?; if config.workers == 0 { bail!("worker count must be > 0"); } Ok(()) }

Option Combinators Over Nested Matching

// Good: Combinator chain
fn find_user_email(users: &[User], id: u64) -> Option<String> {
    users.iter()
        .find(|u| u.id == id)
        .map(|u| u.email.clone())
}

// Bad: Deeply nested matching fn find_user_email_bad(users: &[User], id: u64) -> Option<String> { match users.iter().find(|u| u.id == id) { Some(user) => match &user.email { email => Some(email.clone()), }, None => None, } }

Enums and Pattern Matching

Model States as Enums

// Good: Impossible states are unrepresentable
enum ConnectionState {
    Disconnected,
    Connecting { attempt: u32 },
    Connected { session_id: String },
    Failed { reason: String, retries: u32 },
}

fn handle(state: &ConnectionState) { match state { ConnectionState::Disconnected => connect(), ConnectionState::Connecting { attempt } if *attempt > 3 => abort(), ConnectionState::Connecting { .. } => wait(), ConnectionState::Connected { session_id } => use_session(session_id), ConnectionState::Failed { retries, .. } if *retries < 5 => retry(), ConnectionState::Failed { reason, .. } => log_failure(reason), } }

Exhaustive Matching — No Catch-All for Business Logic

// Good: Handle every variant explicitly
match command {
    Command::Start => start_service(),
    Command::Stop => stop_service(),
    Command::Restart => restart_service(),
    // Adding a new variant forces handling here
}

// Bad: Wildcard hides new variants match command { Command::Start => start_service(), _ => {} // Silently ignores Stop, Restart, and future variants }

Traits and Generics

Accept Generics, Return Concrete Types

// Good: Generic input, concrete output
fn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
    let mut buf = Vec::new();
    reader.read_to_end(&mut buf)?;
    Ok(buf)
}

// Good: Trait bounds for multiple constraints fn process<T: Display + Send + 'static>(item: T) -> String { format!("processed: {item}") }

Trait Objects for Dynamic Dispatch

// Use when you need heterogeneous collections or plugin systems
trait Handler: Send + Sync {
    fn handle(&self, request: &Request) -> Response;
}

struct Router { handlers: Vec<Box<dyn Handler>>, }

// Use generics when you need performance (monomorphization) fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response { handler.handle(request) }

Newtype Pattern for Type Safety

// Good: Distinct types prevent mixing up arguments
struct UserId(u64);
struct OrderId(u64);

fn get_order(user: UserId, order: OrderId) -> Result<Order> { // Can't accidentally swap user and order IDs todo!() }

// Bad: Easy to swap arguments fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> { todo!() }

Structs and Data Modeling

Builder Pattern for Complex Construction

struct ServerConfig {
    host: String,
    port: u16,
    max_connections: usize,
}

impl ServerConfig { fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder { ServerConfigBuilder { host: host.into(), port, max_connections: 100 } } }

struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }

impl ServerConfigBuilder { fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self } fn build(self) -> ServerConfig { ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections } } }

// Usage: ServerConfig::builder("localhost", 8080).max_connections(200).build()

Iterators and Closures

Prefer Iterator Chains Over Manual Loops

// Good: Declarative, lazy, composable
let active_emails: Vec<String> = users.iter()
    .filter(|u| u.is_active)
    .map(|u| u.email.clone())
    .collect();

// Bad: Imperative accumulation let mut active_emails = Vec::new(); for user in &users { if user.is_active { active_emails.push(user.email.clone()); } }

Use collect() with Type Annotation

// Collect into different types
let names: Vec<_> = items.iter().map(|i| &i.name).collect();
let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();
let combined: String = parts.iter().copied().collect();

// Collect Results — short-circuits on first error let parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();

Concurrency

Arc> for Shared Mutable State

use std::sync::{Arc, Mutex};

let counter = Arc::new(Mutex::new(0)); let handles: Vec<_> = (0..10).map(|_| { let counter = Arc::clone(&counter); std::thread::spawn(move || { let mut num = counter.lock().expect("mutex poisoned"); *num += 1; }) }).collect();

for handle in handles { handle.join().expect("worker thread panicked"); }

Channels for Message Passing

use std::sync::mpsc;

let (tx, rx) = mpsc::sync_channel(16); // Bounded channel with backpressure

for i in 0..5 { let tx = tx.clone(); std::thread::spawn(move || { tx.send(format!("message {i}")).expect("receiver disconnected"); }); } drop(tx); // Close sender so rx iterator terminates

for msg in rx { println!("{msg}"); }

Async with Tokio

use tokio::time::Duration;

async fn fetch_with_timeout(url: &str) -> Result<String> { let response = tokio::time::timeout( Duration::from_secs(5), reqwest::get(url), ) .await .context("request timed out")? .context("request failed")?;

response.text().await.context("failed to read body") }

// Spawn concurrent tasks async fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> { let handles: Vec<_> = urls.into_iter() .map(|url| tokio::spawn(async move { fetch_with_timeout(&url).await })) .collect();

let mut results = Vec::with_capacity(handles.len()); for handle in handles { results.push(handle.await.unwrap_or_else(|e| panic!("spawned task panicked: {e}"))); } results }

Unsafe Code

When Unsafe Is Acceptable

// Acceptable: FFI boundary with documented invariants (Rust 2024+)
/// # Safety
/// ptr must be a valid, aligned pointer to an initialized Widget.
unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {
    // SAFETY: caller guarantees ptr is valid and aligned
    unsafe { &*ptr }
}

// Acceptable: Performance-critical path with proof of correctness // SAFETY: index is always < len due to the loop bound unsafe { slice.get_unchecked(index) }

When Unsafe Is NOT Acceptable

// Bad: Using unsafe to bypass borrow checker
// Bad: Using unsafe for convenience
// Bad: Using unsafe without a Safety comment
// Bad: Transmuting between unrelated types

Module System and Crate Structure

Organize by Domain, Not by Type

my_app/
├── src/
│   ├── main.rs
│   ├── lib.rs
│   ├── auth/          # Domain module
│   │   ├── mod.rs
│   │   ├── token.rs
│   │   └── middleware.rs
│   ├── orders/        # Domain module
│   │   ├── mod.rs
│   │   ├── model.rs
│   │   └── service.rs
│   └── db/            # Infrastructure
│       ├── mod.rs
│       └── pool.rs
├── tests/             # Integration tests
├── benches/           # Benchmarks
└── Cargo.toml

Visibility — Expose Minimally

// Good: pub(crate) for internal sharing
pub(crate) fn validate_input(input: &str) -> bool {
    !input.is_empty()
}

// Good: Re-export public API from lib.rs pub mod auth; pub use auth::AuthMiddleware;

// Bad: Making everything pub pub fn internal_helper() {} // Should be pub(crate) or private

Tooling Integration

Essential Commands

# Build and check
cargo build
cargo check              # Fast type checking without codegen
cargo clippy             # Lints and suggestions
cargo fmt                # Format code

Testing

cargo test cargo test -- --nocapture # Show println output cargo test --lib # Unit tests only cargo test --test integration # Integration tests only

Dependencies

cargo audit # Security audit cargo tree # Dependency tree cargo update # Update dependencies

Performance

cargo bench # Run benchmarks

Quick Reference: Rust Idioms

| Idiom | Description | |-------|-------------| | Borrow, don't clone | Pass &T instead of cloning unless ownership is needed | | Make illegal states unrepresentable | Use enums to model valid states only | | ? over unwrap() | Propagate errors, never panic in library/production code | | Parse, don't validate | Convert unstructured data to typed structs at the boundary | | Newtype for type safety | Wrap primitives in newtypes to prevent argument swaps | | Prefer iterators over loops | Declarative chains are clearer and often faster | | #[must_use] on Results | Ensure callers handle return values | | Cow for flexible ownership | Avoid allocations when borrowing suffices | | Exhaustive matching | No wildcard _ for business-critical enums | | Minimal pub surface | Use pub(crate) for internal APIs |

Anti-Patterns to Avoid

// Bad: .unwrap() in production code
let value = map.get("key").unwrap();

// Bad: .clone() to satisfy borrow checker without understanding why let data = expensive_data.clone(); process(&original, &data);

// Bad: Using String when &str suffices fn greet(name: String) { /* should be &str */ }

// Bad: Box<dyn Error> in libraries (use thiserror instead) fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }

// Bad: Ignoring must_use warnings let _ = validate(input); // Silently discarding a Result

// Bad: Blocking in async context async fn bad_async() { std::thread::sleep(Duration::from_secs(1)); // Blocks the executor! // Use: tokio::time::sleep(Duration::from_secs(1)).await; }

Remember: If it compiles, it's probably correct — but only if you avoid unwrap(), minimize unsafe, and let the type system work for you.