Rust 2024 Core Patterns
Quick Start
toml
# Cargo.toml - essential stack
[dependencies]
tokio = { version = "1", features = ["full"] }
thiserror = "2"
anyhow = "2"
serde = { version = "1", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[dev-dependencies]
rstest = "0.22"
proptest = "1"
mockall = "0.13"
insta = "1"
criterion = { version = "0.5", features = ["html_reports"] }
bash
cargo add tokio --features full && cargo add thiserror anyhow serde --features serde/derive
cargo clippy -- -D warnings # enforce in CI
cargo test # unit + integration + doctests
cargo test --doc # doctests only
Idiomatic Patterns
- Builder pattern: Use consuming
self methods; finish with build() -> Result<T, E> for validated construction. - Newtype pattern:
struct UserId(u64) gives type safety at zero cost; add impl From<u64> for UserId. - From/Into: Implement
From<T> only — Into<T> is blanket-provided; accept impl Into<T> in function params. - Derive by default:
#[derive(Debug, Clone, PartialEq, Eq, Hash)] on domain types; add serde only when needed. - Type state: Encode state machines as generic marker types — invalid transitions become compile errors, not panics.
- Borrowed params: Accept
&str not &String, &[T] not &Vec<T>, impl AsRef<Path> not &PathBuf. - Default for config:
Config { field: value, ..Default::default() } — audit all fields when adding new ones. - RAII guards: Use
Drop for resource cleanup; wrap locks, file handles, and connections in guard types. - `Cow<'_, str>`: Return
Cow<'_, str> when sometimes borrowing, sometimes owning — avoids unnecessary clones. - Zero-cost abstractions: Iterator chains compile to identical (often better) machine code than manual loops.
- Stack vs heap: Default to stack; use
Box only for trait objects, recursive types, or large data escaping scope.
Error Handling
thiserror (libraries)
rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum UserError {
#[error("user {id} not found")]
NotFound { id: u64 },
#[error("invalid email: {0}")]
InvalidEmail(String),
#[error("database error")]
Database(#[from] sqlx::Error),
#[error("serialization failed")]
Serialization(#[from] serde_json::Error),
}
Rules:
- Libraries expose typed error enums — callers can pattern-match variants.
- Never use
String or Box<dyn Error> in public library API. #[from] generates From<SourceError> — use ? to propagate automatically.#[source] exposes the underlying error via Error::source() without auto-From.
anyhow (applications / CLIs)
rust
use anyhow::{Context, Result};
fn load_config(path: &Path) -> Result<Config> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading config from {}", path.display()))?;
let config: Config = toml::from_str(&text)
.context("parsing config as TOML")?;
Ok(config)
}
fn main() -> Result<()> {
let config = load_config(Path::new("config.toml"))?;
run(config)
}
Rules:
anyhow::Result<T> in main(), CLI handlers, and top-level application code.- Chain context with
.context("what we were doing") at each call site. - Never use
anyhow in public library APIs — wrap and convert at the boundary.
Error conversion at library/app boundary
rust
// In application code: convert library error to anyhow
let user = user_service.get(id)
.await
.map_err(|e| anyhow::anyhow!("get user {id}: {e}"))?;
// Or implement From in your app error type
impl From<UserError> for AppError { ... }
Traits and Generics
Small, focused traits
rust
// Prefer small traits over large catch-all interfaces
trait Fetch {
async fn fetch(&self, url: &str) -> Result<Bytes, FetchError>;
}
trait Store {
async fn store(&self, key: &str, value: &[u8]) -> Result<(), StoreError>;
}
// Compose with supertraits
trait Cache: Fetch + Store + Send + Sync {}
Trait objects vs generics
rust
// Generics: compile-time dispatch, monomorphized, no heap alloc
fn process<F: Fetch>(fetcher: &F) -> Result<()> { ... }
// Trait objects: runtime dispatch, heap alloc, heterogeneous collections
fn process_dyn(fetcher: &dyn Fetch) -> Result<()> { ... }
fn process_arc(fetcher: Arc<dyn Fetch>) -> Result<()> { ... }
// Rule: default to generics; use dyn Trait for:
// - Heterogeneous collections (Vec<Box<dyn Trait>>)
// - Runtime polymorphism (DI containers, plugin systems)
// - Avoiding monomorphization bloat in large codebases
Associated types vs generics
rust
// Associated type: one implementation per type (Iterator pattern)
trait Parser {
type Output;
fn parse(&self, input: &str) -> Result<Self::Output, ParseError>;
}
// Generic parameter: multiple implementations per type
trait Convert<T> {
fn convert(&self) -> T;
}
// allows: impl Convert<String> for MyType AND impl Convert<u64> for MyType
Generic bounds and where clauses
rust
// Inline bounds for simple cases
fn clone_and_display<T: Clone + Display>(value: &T) { ... }
// Where clause for complex cases (preferred for readability)
fn process<T, E>(items: &[T]) -> Result<Vec<String>, E>
where
T: Display + Send + 'static,
E: std::error::Error + From<fmt::Error>,
{ ... }
Blanket implementations
rust
// Provide default impl for all qualifying types
trait Summarize {
fn summary(&self) -> String;
}
impl<T: Display> Summarize for T {
fn summary(&self) -> String {
format!("{}", self)
}
}
Extension traits (adding methods to foreign types)
rust
trait IteratorExt: Iterator {
fn take_while_inclusive<P>(self, predicate: P) -> TakeWhileInclusive<Self, P>
where
P: FnMut(&Self::Item) -> bool,
Self: Sized;
}
impl<I: Iterator> IteratorExt for I {
fn take_while_inclusive<P>(self, predicate: P) -> TakeWhileInclusive<Self, P>
where P: FnMut(&Self::Item) -> bool, Self: Sized {
TakeWhileInclusive { iter: self, predicate, done: false }
}
}
Sealed traits (prevent external implementation)
rust
mod private {
pub trait Sealed {}
}
pub trait MyTrait: private::Sealed {
fn method(&self);
}
// Only types you impl private::Sealed for can impl MyTrait
impl private::Sealed for MyType {}
impl MyTrait for MyType { fn method(&self) {} }
Generic Associated Types (GATs)
rust
// Stable since Rust 1.65 — enables lifetime-parameterized associated types
trait Lender {
type Item<'a> where Self: 'a;
fn lend(&mut self) -> Self::Item<'_>;
}
Macros
Declarative macros (macro_rules!)
rust
// Pattern matching on token trees
macro_rules! hashmap {
($($key:expr => $val:expr),* $(,)?) => {{
let mut map = std::collections::HashMap::new();
$(map.insert($key, $val);)*
map
}};
}
let m = hashmap!{ "a" => 1, "b" => 2 };
// Repeat patterns
macro_rules! impl_from_str {
($($t:ty),+) => {
$(impl std::str::FromStr for $t {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<u64>().map(Self).map_err(Into::into)
}
})+
};
}
Procedural macros (derive)
rust
// Custom derive — separate crate required (e.g., my-macros-derive)
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
let expanded = quote! {
impl #name {
pub fn builder() -> #nameBuilder { Default::default() }
}
};
expanded.into()
}
Rules:
- Use declarative macros for simple repetition; proc macros for derive and attribute macros.
- Document macro hygiene:
$crate:: prefix for items referenced inside the macro. - Prefer
derive_more and strum crates over writing your own derive macros. - Test macros via
trybuild to verify compile-time error messages.
Async / Concurrency Patterns
Tokio fundamentals
rust
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Tokio only — async-std is effectively unmaintained as of 2026
run().await
}
// Async closures (Rust 2024)
let handler = async |req: Request| -> Result<Response> {
process(req).await
};
Structured concurrency
rust
use tokio::{join, select};
// Concurrent execution (all complete)
let (users, orders) = join!(fetch_users(), fetch_orders());
// Racing (first to complete wins)
select! {
result = fetch_with_primary() => result,
result = fetch_with_fallback() => result,
_ = tokio::time::sleep(Duration::from_secs(5)) => Err(TimeoutError),
}
// Dynamic task groups
use tokio::task::JoinSet;
let mut set = JoinSet::new();
for id in ids {
set.spawn(async move { fetch_item(id).await });
}
while let Some(result) = set.join_next().await {
handle(result??);
}
Shared state patterns
rust
// Arc<Mutex<T>> for mutable shared state
let state: Arc<Mutex<HashMap<u64, User>>> = Arc::new(Mutex::new(HashMap::new()));
let state_clone = Arc::clone(&state);
tokio::spawn(async move {
let mut guard = state_clone.lock().await; // tokio::sync::Mutex
guard.insert(1, user);
});
// RwLock for read-heavy workloads
let cache: Arc<RwLock<HashMap<String, Value>>> = Arc::new(RwLock::new(HashMap::new()));
let val = cache.read().await.get("key").cloned();
// Atomics for simple flags/counters — no mutex overhead
use std::sync::atomic::{AtomicU64, Ordering};
let counter = Arc::new(AtomicU64::new(0));
counter.fetch_add(1, Ordering::SeqCst);
Actor pattern (message-passing concurrency)
rust
use tokio::sync::mpsc;
enum Command {
Get { key: String, reply: tokio::sync::oneshot::Sender<Option<String>> },
Set { key: String, value: String },
Shutdown,
}
struct CacheActor {
store: HashMap<String, String>,
rx: mpsc::Receiver<Command>,
}
impl CacheActor {
async fn run(mut self) {
while let Some(cmd) = self.rx.recv().await {
match cmd {
Command::Get { key, reply } => {
let _ = reply.send(self.store.get(&key).cloned());
}
Command::Set { key, value } => {
self.store.insert(key, value);
}
Command::Shutdown => break,
}
}
}
}
// Handle is cheaply cloneable — clone per task/thread
#[derive(Clone)]
struct CacheHandle(mpsc::Sender<Command>);
impl CacheHandle {
async fn get(&self, key: String) -> Option<String> {
let (tx, rx) = tokio::sync::oneshot::channel();
self.0.send(Command::Get { key, reply: tx }).await.ok()?;
rx.await.ok()?
}
async fn set(&self, key: String, value: String) {
let _ = self.0.send(Command::Set { key, value }).await;
}
}
Cancellation safety
rust
// RAII cleanup: use Drop for cancellation handling
struct Guard {
resource: Resource,
}
impl Drop for Guard {
fn drop(&mut self) {
// Always runs, even if task is cancelled
self.resource.cleanup();
}
}
// Avoid holding mutable state across .await points
// BAD:
let mut locked = mutex.lock().await;
do_io().await; // lock held across await — deadlock risk
drop(locked);
// GOOD:
{
let mut locked = mutex.lock().await;
update_state(&mut locked);
} // lock released before await
do_io().await;
CPU-bound work
rust
// Never block the async executor
let result = tokio::task::spawn_blocking(|| {
// CPU-intensive or blocking sync code here
heavy_computation()
})
.await?;