--- title: "Rust Best Practices" description: "Idiomatic Rust patterns for ownership and borrowing, error handling with thiserror and anyhow, choosing between traits/generics/dyn dispatch, and async programming with Tokio, targeting production-grade Rust codebases in 2026." language: rust framework: null category: language_best_practices tags: - best-practices - ownership - borrowing - error-handling - thiserror - anyhow - async - tokio - traits - generics - concurrency keywords: - rust error handling thiserror vs anyhow - rust question mark operator error propagation - traits vs generics vs dyn dispatch rust - async rust tokio best practices 2026 - resolving rust borrow checker errors idiomatically last_updated: 2026-07-07 difficulty: intermediate version: "Rust >= 1.90, Edition 2024, Tokio >= 1.40" related: - ./README.md - ../go/README.md - ../go/best_practices.md - ../python/README.md - ../python/frameworks/fastapi/best_practices.md - ../../core/principles/solid.md - ../../core/architecture/clean_architecture.md - ../../core/devops/autonomous_least_privilege_environments.md search_priority: high status: published --- # Rust Best Practices ## Introduction / Overview Writing idiomatic Rust means designing with ownership rather than around it, choosing the right error-handling strategy for the audience of your code (library versus application), and picking the correct dispatch mechanism (static generics versus dynamic `dyn Trait`) for the performance and flexibility trade-offs your problem demands. In 2026, with the 2024 edition stable and async closures shipped, the ecosystem finally offers ergonomic answers to most of the friction that historically made Rust feel harder than it needed to be. This document assumes familiarity with the concepts in [`README.md`](./README.md) (ownership, borrowing, lifetimes) and focuses on applying them correctly in real codebases: structuring errors, choosing abstractions, and writing correct, efficient async code with Tokio. **When to use this guidance**: You are writing production Rust - a library crate, a CLI tool, or an async network service - and want patterns that hold up as the codebase and team grow. ## Core Concepts - **Ownership-first design**: Structure data flow so that a single, clear owner exists for each resource at each point in time; pass borrows (`&T`, `&mut T`) to functions that only need temporary access. Reach for shared ownership (`Rc`, `Arc`) only when the domain genuinely requires multiple long-lived owners. - **Fallible vs infallible operations**: Rust makes failure explicit via `Result` and absence explicit via `Option`. There are no exceptions for recoverable errors and no implicit `null`. - **Static vs dynamic polymorphism**: Generics with trait bounds are resolved and monomorphized at compile time (zero runtime cost, larger binaries); `dyn Trait` is resolved via a vtable at runtime (smaller binaries, one indirect call, heterogeneous collections). - **`Send` and `Sync`**: Marker traits the compiler uses to guarantee thread safety. A type is `Send` if it can be transferred across threads; `Sync` if references to it can be shared across threads. Async runtimes and thread-spawning APIs are bounded by these traits, catching data races at compile time. - **Async is cooperative, not preemptive**: `async fn` compiles to a state machine that yields at `.await` points. Blocking the executor thread (e.g., calling synchronous I/O inside an `async fn`) stalls every other task on that thread - this is the single most common async Rust bug. ## Best Practices ### 1. Use `thiserror` for Library Errors, `anyhow` for Application Errors **Rationale**: Library callers need to pattern-match on specific failure modes to recover programmatically; application code mostly needs to log, add context, and propagate. `thiserror` derives `std::error::Error` on your own enums with zero boilerplate. `anyhow::Error` is a type-erased, dynamically dispatched error container ideal for `main()` and top-level handlers where the concrete type rarely matters. ```rust // lib/src/error.rs - public library error type, matchable by callers use thiserror::Error; #[derive(Debug, Error)] pub enum OrderError { #[error("order {order_id} not found")] NotFound { order_id: u64 }, #[error("insufficient inventory for sku {sku}: requested {requested}, available {available}")] InsufficientInventory { sku: String, requested: u32, available: u32 }, #[error("database error")] Database(#[from] sqlx::Error), } ``` ```rust // bin/src/main.rs - application code, aggregates and contextualizes errors use anyhow::{Context, Result}; async fn run() -> Result<()> { let config = load_config("config.toml") .context("failed to load application configuration")?; let pool = connect_db(&config.database_url) .await .context("failed to establish database connection pool")?; Ok(()) } ``` **Why this works well in production**: callers of `OrderError` can `match` on `InsufficientInventory` to render a specific HTTP 409, while `main()` never has to enumerate every possible failure mode across every dependency - it just logs the full context chain via `{:#}`. ### 2. Propagate Errors with `?`, Reserve `unwrap()`/`expect()` for True Invariants **Rationale**: The `?` operator converts and propagates errors with a single character, keeping the happy path readable while still forcing every fallible call site to be handled or explicitly deferred. ```rust async fn place_order(pool: &PgPool, sku: &str, qty: u32) -> Result { let available = fetch_inventory(pool, sku).await?; if available < qty { return Err(OrderError::InsufficientInventory { sku: sku.to_string(), requested: qty, available, }); } let order_id = insert_order(pool, sku, qty).await?; Ok(order_id) } ``` **Why this works well in production**: every error path is visible in the function's return type; there is no hidden control flow, and `cargo clippy` flags unhandled `Result`s (`#[must_use]`). ### 3. Choose Traits, Generics, or `dyn Trait` Deliberately **Rationale**: These three tools solve overlapping problems with different trade-offs. Defaulting to one everywhere leads either to bloated binaries and unreadable trait-bound soup, or to unnecessary runtime indirection. | Mechanism | Dispatch | Binary size impact | Use when | |-------------------|-----------|---------------------|----------| | Generic + trait bound (`fn f(x: T)`) | Static (monomorphized) | Larger (one copy per type) | Hot paths, small number of concrete types, want inlining | | `impl Trait` (arg or return position) | Static | Larger | Simplify signatures without naming the concrete type | | `dyn Trait` / `Box` | Dynamic (vtable) | Smaller (one shared copy) | Heterogeneous collections, plugin-style APIs, reducing compile times | ```rust trait PaymentGateway { fn charge(&self, amount_cents: u64) -> Result<(), PaymentError>; } // Static dispatch: fastest, one specialized copy per gateway type used fn checkout_static(gateway: &G, amount: u64) -> Result<(), PaymentError> { gateway.charge(amount) } // Dynamic dispatch: one function body, heterogeneous list of gateways fn checkout_dynamic(gateway: &dyn PaymentGateway, amount: u64) -> Result<(), PaymentError> { gateway.charge(amount) } let gateways: Vec> = vec![Box::new(StripeGateway), Box::new(PaypalGateway)]; ``` **Why this works well in production**: reserving `dyn Trait` for genuinely polymorphic call sites (payment gateway selected at runtime by config) keeps hot-path code monomorphized while avoiding combinatorial compile-time blowup elsewhere. ### 4. Structure Async Code Around Tokio's Task Model **Rationale**: Tokio is the de facto standard async runtime in 2026 (multi-threaded work-stealing scheduler, mature ecosystem: `axum`, `tonic`, `sqlx`, `tower`). Correct use means never blocking the executor and using structured concurrency primitives instead of unmanaged `tokio::spawn` calls. ```rust use tokio::task::JoinSet; use std::time::Duration; #[tokio::main] async fn main() -> anyhow::Result<()> { let mut tasks = JoinSet::new(); for shard_id in 0..4 { tasks.spawn(async move { process_shard(shard_id).await }); } while let Some(result) = tasks.join_next().await { match result { Ok(Ok(())) => tracing::info!("shard completed"), Ok(Err(e)) => tracing::error!(error = %e, "shard failed"), Err(join_err) => tracing::error!(error = %join_err, "task panicked"), } } Ok(()) } async fn process_shard(shard_id: u32) -> anyhow::Result<()> { tokio::time::timeout(Duration::from_secs(30), do_work(shard_id)).await??; Ok(()) } ``` **Why this works well in production**: - `JoinSet` gives structured concurrency - every spawned task's completion (or panic) is observed, unlike bare `tokio::spawn` calls whose `JoinHandle` is dropped and forgotten. - `tokio::time::timeout` bounds every external call, preventing one slow dependency from exhausting the runtime's worker threads. - Use `tokio::task::spawn_blocking` for any CPU-bound or synchronous-blocking work (e.g., calling a synchronous C library, heavy hashing) to avoid starving the async scheduler. ### 5. Resolving Common Borrow Checker Friction **Rationale**: Most borrow-checker errors map to a small set of recurring patterns. Recognizing them lets you fix the design instead of reaching for `.clone()`. ```rust // Friction: iterating and mutating the same collection let mut orders = vec![Order::new(1), Order::new(2)]; // Fails: cannot borrow `orders` as mutable while iterating an immutable borrow // for order in &orders { orders.push(order.clone()); } // Fix: collect indices or new values first, mutate after the immutable borrow ends let overdue_ids: Vec = orders.iter().filter(|o| o.is_overdue()).map(|o| o.id).collect(); for id in overdue_ids { mark_escalated(&mut orders, id); } ``` ```rust // Friction: returning a reference to a local value // fn make_greeting() -> &str { let s = String::from("hi"); &s } // fails: `s` dropped at end of scope // Fix: return owned data, let the caller decide whether to borrow or store it fn make_greeting() -> String { String::from("hi") } ``` ```rust // Friction: shared mutable state across async tasks use std::sync::Arc; use tokio::sync::Mutex; let shared_counter = Arc::new(Mutex::new(0u64)); let counter = Arc::clone(&shared_counter); tokio::spawn(async move { let mut guard = counter.lock().await; *guard += 1; }); ``` **Why this works well in production**: each pattern above reflects an actual ownership question (who mutates when, who owns the data after this function returns, who needs shared access across concurrent tasks) rather than a compiler technicality - resolving it explicitly produces more maintainable code than clone-and-hope. ## Common Pitfalls & Anti-Patterns ### Pitfall: Leaking `anyhow::Error` from a Public Library API **Problem**: Callers receive an opaque, type-erased error and cannot `match` on failure modes, forcing them to parse error message strings. **Recommended approach**: Public library functions return a crate-specific `Result` built with `thiserror`. Convert to `anyhow::Error` only at the application boundary, if at all. ### Pitfall: Blocking the Async Executor **Problem**: Calling `std::thread::sleep`, synchronous file I/O, or a CPU-heavy loop directly inside an `async fn` stalls every other task scheduled on that worker thread, causing latency spikes that are hard to diagnose. **Recommended approach**: Use `tokio::time::sleep` for delays, `tokio::fs` for async file I/O, and `tokio::task::spawn_blocking` to offload CPU-bound work to a dedicated blocking thread pool. ### Pitfall: `Rc>` as a Default Solution to Ownership Errors **Problem**: Wrapping every shared struct in `Rc>` (or `Arc>` in async code) defers ownership decisions to runtime, reintroduces the possibility of panics (`RefCell` borrow conflicts) or deadlocks, and often signals a design that fights the ownership model rather than embracing it. **Recommended approach**: Prefer passing data by value or reference along a clear ownership hierarchy; reserve interior mutability for genuinely shared, long-lived state (e.g., a connection pool, a cache). ### Pitfall: Overusing Macros for Code That Generics Handle Fine **Problem**: `macro_rules!` and procedural macros trade compile-time safety and IDE support for terse syntax, and are harder to debug than an equivalent generic function. **Recommended approach**: Reach for macros only when generics genuinely cannot express the pattern (e.g., generating repetitive trait impls across many types, or derive-style boilerplate elimination). ## Testing Strategies - **Unit tests** (`#[cfg(test)] mod tests`) for pure business logic - no I/O, no async runtime needed for most of these. - **Async tests**: `#[tokio::test]` spins up a runtime per test; use `#[tokio::test(flavor = "multi_thread")]` when testing genuinely concurrent behavior. - **Mocking traits**: define narrow traits for external dependencies (database, HTTP client) and provide in-memory fake implementations for tests - Rust's trait objects make this a natural fit, no separate mocking framework required for most cases. `mockall` is available for generating mocks from trait definitions when hand-written fakes become repetitive. - **Property-based testing**: `proptest` for parsers, serializers, and any function with a clear mathematical invariant (round-trip encode/decode, sort stability). - **Integration tests**: place in `tests/`, spin up a real (or containerized, via `testcontainers`) database for end-to-end coverage of the public API surface. ```rust #[tokio::test] async fn insufficient_inventory_returns_typed_error() { let pool = test_pool().await; seed_inventory(&pool, "SKU-1", 2).await; let result = place_order(&pool, "SKU-1", 10).await; assert!(matches!(result, Err(OrderError::InsufficientInventory { .. }))); } ``` ## Performance, Security & Scalability Considerations - **Avoid unnecessary allocation**: prefer `&str` over `String`, `&[T]` over `Vec` in function signatures when ownership is not required; use `Cow` when a function sometimes needs to allocate and sometimes doesn't. - **Profile before optimizing**: use `cargo flamegraph` or `perf` to find real hot paths rather than guessing; Rust's zero-cost abstractions mean idiomatic code is usually already fast. - **Supply chain security**: run `cargo audit` (RustSec advisories) and `cargo deny check` in CI on every dependency update. Install both with `cargo install --locked cargo-audit cargo-deny` - this places the binaries in `~/.cargo/bin` under the invoking user's own account, requiring no `sudo` and working identically on a developer laptop, a CI runner, or an AI agent's sandbox regardless of OS (see [Autonomous, Least-Privilege, Portable Execution Environments](../../core/devops/autonomous_least_privilege_environments.md)); `--locked` pins the tool's own dependency versions for a reproducible, non-interactive install. - **Panics as a denial-of-service vector**: an unhandled `panic!` in a request-handling task can, depending on the runtime setup, take down more than the single request - use `catch_unwind` at task boundaries in servers, or rely on Tokio's per-task isolation (a panic in one spawned task does not crash the process, only that task). - **Backpressure**: use bounded channels (`tokio::sync::mpsc::channel(capacity)`) rather than unbounded ones to prevent memory exhaustion under load spikes. - **Horizontal scalability**: async services built on Tokio + `axum`/`tonic` scale well per-instance (high connection concurrency on modest hardware) and are naturally stateless when following the ownership-first design above, simplifying horizontal scaling behind a load balancer. ## Edge Cases & Advanced Usage - **Self-referential structs**: not directly expressible safely; use arenas (`typed-arena`), indices instead of pointers, or the `Pin`/`ouroboros` crate for the rare cases that truly need them. - **Cancellation safety in async code**: a future dropped mid-`.await` (e.g., due to `tokio::select!` or a timeout) must leave shared state consistent - audit any `async fn` that mutates shared state across an `.await` point for cancellation safety. - **FFI boundaries**: when exposing Rust to C or embedding C libraries, isolate all `unsafe` in a thin `sys` crate and expose a safe, idiomatic wrapper crate on top - the standard two-crate FFI pattern (e.g., `openssl-sys` + `openssl`). - **Const generics**: enable compile-time-sized arrays and buffers parameterized by integers (`[T; N]`), useful for embedded and cryptographic code that must avoid heap allocation entirely. - **Workspace-wide lint enforcement**: use a workspace-level `Cargo.toml` `[workspace.lints]` table to enforce `clippy::pedantic` selectively across every crate in a monorepo. ## When *Not* to Use / Alternatives | Dimension | Rust (idiomatic, as above) | Go equivalent | Winner | |-----------------------------------|-------------------------------|----------------------------------------|---------------| | Error handling verbosity | Explicit, typed, more upfront design | Explicit but simpler (`if err != nil`) | Go for simplicity | | Compile-time correctness guarantees | Strongest (ownership, `Send`/`Sync`) | Weaker (races caught at runtime by `-race`) | Rust | | Iteration speed for CRUD services | Slower (borrow checker, compile times) | Faster | Go | | Async ecosystem maturity | Strong (Tokio) but more manual reasoning | Simpler (goroutines are implicit async) | Go | | Long-term refactor safety | Excellent (compiler catches breakage) | Good, but fewer compile-time guarantees | Rust | **Choose a garbage-collected language** (Go, see [`../go/best_practices.md`](../go/best_practices.md)) when team velocity and simplicity of the concurrency model matter more than eliminating an entire class of memory bugs the team may rarely encounter in practice. **Choose Python + FastAPI** (see [`../python/frameworks/fastapi/best_practices.md`](../python/frameworks/fastapi/best_practices.md)) for rapid iteration on APIs where request throughput is not the binding constraint. ## Related Documentation - [Rust Language Overview & Ecosystem](./README.md) - [Go Language Overview](../go/README.md) - [Go Best Practices](../go/best_practices.md) - [Python Language Overview](../python/README.md) - [FastAPI Best Practices](../python/frameworks/fastapi/best_practices.md) - [SOLID Principles](../../core/principles/solid.md) - [Clean Architecture](../../core/architecture/clean_architecture.md) - [Autonomous, Least-Privilege, Portable Execution Environments](../../core/devops/autonomous_least_privilege_environments.md) ## Glossary - **`Result`**: The type representing either success (`Ok(T)`) or failure (`Err(E)`); Rust's primary mechanism for recoverable errors. - **`?` operator**: Syntax that propagates an `Err`/`None` early from the current function, converting the error type via `From` if needed. - **`thiserror`**: A derive macro crate for defining structured, matchable error enums with minimal boilerplate - intended for libraries. - **`anyhow`**: A type-erased, dynamically dispatched error container with `.context()` chaining - intended for applications. - **Monomorphization**: Compile-time generation of a specialized function/struct body per concrete generic type parameter. - **`dyn Trait`**: A trait object enabling runtime polymorphism via vtable dispatch, at the cost of an indirect call and heap allocation (typically behind `Box`). - **`Send` / `Sync`**: Compiler-checked marker traits guaranteeing a type is safe to move across threads (`Send`) or share by reference across threads (`Sync`). - **Structured concurrency**: A concurrency style (e.g., Tokio's `JoinSet`) where every spawned task's lifetime and result are explicitly tracked by its parent, preventing orphaned or silently-failing tasks. --- **Maintenance note**: When updating this document, also update `last_updated` and re-verify recommended crate versions (`tokio`, `thiserror`, `anyhow`, `axum`) against their current published releases on crates.io.