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
) -> 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 ///
ptrmust be a valid, aligned pointer to an initializedWidget. 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
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.