Rust Pro
"In Rust, the compiler is your strictest code reviewer and your most reliable safety net."
When to Use
- Writing new Rust code or modules
- Reviewing Rust code for idiomatic patterns
- Debugging ownership, borrowing, or lifetime errors
- Implementing async Rust with Tokio or async-std
- Designing concurrent systems with Rust's guarantees
- Choosing between Rust abstractions (trait objects vs. generics, Arc vs. Rc)
Ownership and Borrowing
Core Rules
- Each value has exactly one owner
- When the owner goes out of scope, the value is dropped
- You can have either one mutable reference OR any number of immutable references (not both)
When to Clone vs. Borrow
- Borrow (&T or &mut T) by default; cloning is a last resort, not a first instinct
- Clone when you need independent ownership (e.g., sending data to another thread)
- Clone when the data is small and cheap to copy (small strings, numbers, small structs)
- Use Cow<'_, T> when you sometimes need to own and sometimes need to borrow
Common Ownership Patterns
// Transfer ownership
fn process(data: Vec<u8>) { /* owns data, dropped at end */ }
// Borrow immutably
fn analyze(data: &[u8]) -> usize { data.len() }
// Borrow mutably
fn transform(data: &mut Vec<u8>) { data.push(0); }
// Return owned data
fn create() -> Vec<u8> { vec![1, 2, 3] }
Avoiding Borrow Checker Fights
- Structure code so borrows have clear, non-overlapping scopes
- Split structs if one field's borrow conflicts with another's mutation
- Use indices instead of references into collections you need to mutate
- Consider interior mutability (RefCell, Mutex) when shared mutable state is truly needed
Lifetimes
When Annotations Are Needed
- Function signatures with references in both input and output
- Struct definitions that hold references
- Impl blocks for structs with lifetime parameters
Lifetime Elision Rules
- Each input reference gets its own lifetime parameter
- If there is exactly one input lifetime, it is assigned to all output lifetimes
- If one input is &self or &mut self, its lifetime is assigned to all outputs
Practical Patterns
// Explicit lifetime: output borrows from input
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// Struct holding a reference
struct Parser<'a> {
input: &'a str,
position: usize,
}
// When in doubt, make it owned
// Use String instead of &str in structs unless you have a clear lifetime story
'static Lifetime
- Means the data lives for the entire program duration
- String literals are 'static:
let s: &'static str = "hello";
- Required by many async runtimes for spawned tasks
- Owned types (String, Vec) satisfy 'static bounds because they own their data
Error Handling
The Error Hierarchy
- Use Result<T, E> for recoverable errors; never panic for expected failures
- Use Option for absence of a value (not an error condition)
- Reserve panic! for programmer errors and unrecoverable invariant violations
Custom Error Types
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Not found: {entity} with id {id}")]
NotFound { entity: &'static str, id: String },
#[error("Validation failed: {0}")]
Validation(String),
#[error(transparent)]
Unexpected(#[from] anyhow::Error),
}
Error Handling Best Practices
- Use
thiserror for library error types (structured, typed errors)
- Use
anyhow for application-level error handling (convenient, context-rich)
- Add context with
.context("what was being done") from anyhow
- Use
? operator for propagation; avoid explicit match on every Result
- Map errors at boundaries (e.g., convert DB errors to API errors in the controller layer)
Async Rust
Runtime Choice
- Tokio: most popular, full-featured, best ecosystem support
- async-std: simpler API, mirrors std library naming
- Choose one runtime per project; do not mix
Async Patterns
// Spawning concurrent tasks
let (a, b) = tokio::join!(fetch_users(), fetch_orders());
// Spawning independent tasks
let handle = tokio::spawn(async move {
process(data).await
});
let result = handle.await?;
// Select first to complete
tokio::select! {
result = operation() => handle_result(result),
_ = tokio::time::sleep(Duration::from_secs(5)) => handle_timeout(),
}
Async Pitfalls
- Do not hold a MutexGuard (std or tokio) across an await point
- Use
tokio::sync::Mutex if you must hold a lock across awaits
- Avoid blocking the async runtime: use
tokio::task::spawn_blocking for CPU work
- Ensure futures are Send + 'static when spawning tasks (no non-Send references held across awaits)
- Use
#[tokio::main] on main, not manual runtime building, unless you need custom config
Concurrency Patterns
Shared State
Arc<Mutex<T>> for shared mutable state across threads
Arc<RwLock<T>> when reads vastly outnumber writes
DashMap for concurrent hashmap without manual locking
- Prefer message passing (channels) over shared state when possible
Channels
// mpsc: multiple producers, single consumer
let (tx, mut rx) = tokio::sync::mpsc::channel(100);
// oneshot: single value, single send
let (tx, rx) = tokio::sync::oneshot::channel();
// broadcast: multiple consumers, each gets every message
let (tx, _) = tokio::sync::broadcast::channel(100);
// watch: single producer, multiple consumers, latest value only
let (tx, rx) = tokio::sync::watch::channel(initial_value);
Rayon for CPU Parallelism
- Use Rayon for data-parallel CPU work (parallel iterators)
- Do not use Rayon inside async contexts; use spawn_blocking as a bridge
par_iter(), par_chunks() for embarrassingly parallel operations
Idiomatic Rust
Iterators Over Loops
// Prefer this
let names: Vec<String> = users.iter().map(|u| u.name.clone()).collect();
// Over this
let mut names = Vec::new();
for u in &users {
names.push(u.name.clone());
}
Builder Pattern
- Use for structs with many optional fields
- Return &mut Self for chaining; provide a build() method that validates and returns Result
Newtype Pattern
- Wrap primitive types to add type safety:
struct UserId(u64);
- Prevents mixing up IDs of different entities at compile time
- Implement Deref only if the newtype truly "is a" the inner type
Trait Design
- Keep traits small and focused (Interface Segregation Principle)
- Provide default method implementations where a sensible default exists
- Use associated types over generic parameters when there is one natural choice
- Prefer generic bounds (
impl Trait or where T: Trait) over trait objects (dyn Trait) for performance
Performance Tips
- Prefer &str over String in function parameters
- Use Vec::with_capacity when the size is known in advance
- Avoid unnecessary allocations in hot loops
- Use
#[inline] sparingly; let the compiler decide in most cases
- Profile before optimizing: use cargo flamegraph, criterion for benchmarks
- Prefer stack allocation (arrays, small structs) over heap allocation where possible
Project Structure
src/
main.rs or lib.rs
config.rs
error.rs # Custom error types
models/ # Data structures
handlers/ # Request handlers (for web servers)
services/ # Business logic
db/ # Database access
tests/
integration/ # Integration tests
benches/ # Criterion benchmarks
Source: bugrabilge/bilge-development-kit — distributed by TomeVault.
1---2name: rust-pro-113description: Rust ownership, borrowing, lifetimes, error handling, async programming, concurrency patterns, and idiomatic Rust. Use when writing, reviewing, or debugging Rust code. Use when this capability is needed.4---56# Rust Pro78> "In Rust, the compiler is your strictest code reviewer and your most reliable safety net."910## When to Use11- Writing new Rust code or modules12- Reviewing Rust code for idiomatic patterns13- Debugging ownership, borrowing, or lifetime errors14- Implementing async Rust with Tokio or async-std15- Designing concurrent systems with Rust's guarantees16- Choosing between Rust abstractions (trait objects vs. generics, Arc vs. Rc)1718## Ownership and Borrowing1920### Core Rules211. Each value has exactly one owner222. When the owner goes out of scope, the value is dropped233. You can have either one mutable reference OR any number of immutable references (not both)2425### When to Clone vs. Borrow26- Borrow (&T or &mut T) by default; cloning is a last resort, not a first instinct27- Clone when you need independent ownership (e.g., sending data to another thread)28- Clone when the data is small and cheap to copy (small strings, numbers, small structs)29- Use Cow<'_, T> when you sometimes need to own and sometimes need to borrow3031### Common Ownership Patterns32```rust33// Transfer ownership34fn process(data: Vec<u8>) { /* owns data, dropped at end */ }3536// Borrow immutably37fn analyze(data: &[u8]) -> usize { data.len() }3839// Borrow mutably40fn transform(data: &mut Vec<u8>) { data.push(0); }4142// Return owned data43fn create() -> Vec<u8> { vec![1, 2, 3] }44```4546### Avoiding Borrow Checker Fights47- Structure code so borrows have clear, non-overlapping scopes48- Split structs if one field's borrow conflicts with another's mutation49- Use indices instead of references into collections you need to mutate50- Consider interior mutability (RefCell, Mutex) when shared mutable state is truly needed5152## Lifetimes5354### When Annotations Are Needed55- Function signatures with references in both input and output56- Struct definitions that hold references57- Impl blocks for structs with lifetime parameters5859### Lifetime Elision Rules601. Each input reference gets its own lifetime parameter612. If there is exactly one input lifetime, it is assigned to all output lifetimes623. If one input is &self or &mut self, its lifetime is assigned to all outputs6364### Practical Patterns65```rust66// Explicit lifetime: output borrows from input67fn first_word(s: &str) -> &str {68 s.split_whitespace().next().unwrap_or("")69}7071// Struct holding a reference72struct Parser<'a> {73 input: &'a str,74 position: usize,75}7677// When in doubt, make it owned78// Use String instead of &str in structs unless you have a clear lifetime story79```8081### 'static Lifetime82- Means the data lives for the entire program duration83- String literals are 'static: `let s: &'static str = "hello";`84- Required by many async runtimes for spawned tasks85- Owned types (String, Vec) satisfy 'static bounds because they own their data8687## Error Handling8889### The Error Hierarchy90- Use Result<T, E> for recoverable errors; never panic for expected failures91- Use Option<T> for absence of a value (not an error condition)92- Reserve panic! for programmer errors and unrecoverable invariant violations9394### Custom Error Types95```rust96use thiserror::Error;9798#[derive(Error, Debug)]99pub enum AppError {100 #[error("Database error: {0}")]101 Database(#[from] sqlx::Error),102103 #[error("Not found: {entity} with id {id}")]104 NotFound { entity: &'static str, id: String },105106 #[error("Validation failed: {0}")]107 Validation(String),108109 #[error(transparent)]110 Unexpected(#[from] anyhow::Error),111}112```113114### Error Handling Best Practices115- Use `thiserror` for library error types (structured, typed errors)116- Use `anyhow` for application-level error handling (convenient, context-rich)117- Add context with `.context("what was being done")` from anyhow118- Use `?` operator for propagation; avoid explicit match on every Result119- Map errors at boundaries (e.g., convert DB errors to API errors in the controller layer)120121## Async Rust122123### Runtime Choice124- Tokio: most popular, full-featured, best ecosystem support125- async-std: simpler API, mirrors std library naming126- Choose one runtime per project; do not mix127128### Async Patterns129```rust130// Spawning concurrent tasks131let (a, b) = tokio::join!(fetch_users(), fetch_orders());132133// Spawning independent tasks134let handle = tokio::spawn(async move {135 process(data).await136});137let result = handle.await?;138139// Select first to complete140tokio::select! {141 result = operation() => handle_result(result),142 _ = tokio::time::sleep(Duration::from_secs(5)) => handle_timeout(),143}144```145146### Async Pitfalls147- Do not hold a MutexGuard (std or tokio) across an await point148- Use `tokio::sync::Mutex` if you must hold a lock across awaits149- Avoid blocking the async runtime: use `tokio::task::spawn_blocking` for CPU work150- Ensure futures are Send + 'static when spawning tasks (no non-Send references held across awaits)151- Use `#[tokio::main]` on main, not manual runtime building, unless you need custom config152153## Concurrency Patterns154155### Shared State156- `Arc<Mutex<T>>` for shared mutable state across threads157- `Arc<RwLock<T>>` when reads vastly outnumber writes158- `DashMap` for concurrent hashmap without manual locking159- Prefer message passing (channels) over shared state when possible160161### Channels162```rust163// mpsc: multiple producers, single consumer164let (tx, mut rx) = tokio::sync::mpsc::channel(100);165166// oneshot: single value, single send167let (tx, rx) = tokio::sync::oneshot::channel();168169// broadcast: multiple consumers, each gets every message170let (tx, _) = tokio::sync::broadcast::channel(100);171172// watch: single producer, multiple consumers, latest value only173let (tx, rx) = tokio::sync::watch::channel(initial_value);174```175176### Rayon for CPU Parallelism177- Use Rayon for data-parallel CPU work (parallel iterators)178- Do not use Rayon inside async contexts; use spawn_blocking as a bridge179- `par_iter()`, `par_chunks()` for embarrassingly parallel operations180181## Idiomatic Rust182183### Iterators Over Loops184```rust185// Prefer this186let names: Vec<String> = users.iter().map(|u| u.name.clone()).collect();187188// Over this189let mut names = Vec::new();190for u in &users {191 names.push(u.name.clone());192}193```194195### Builder Pattern196- Use for structs with many optional fields197- Return &mut Self for chaining; provide a build() method that validates and returns Result198199### Newtype Pattern200- Wrap primitive types to add type safety: `struct UserId(u64);`201- Prevents mixing up IDs of different entities at compile time202- Implement Deref only if the newtype truly "is a" the inner type203204### Trait Design205- Keep traits small and focused (Interface Segregation Principle)206- Provide default method implementations where a sensible default exists207- Use associated types over generic parameters when there is one natural choice208- Prefer generic bounds (`impl Trait` or `where T: Trait`) over trait objects (`dyn Trait`) for performance209210## Performance Tips211- Prefer &str over String in function parameters212- Use Vec::with_capacity when the size is known in advance213- Avoid unnecessary allocations in hot loops214- Use `#[inline]` sparingly; let the compiler decide in most cases215- Profile before optimizing: use cargo flamegraph, criterion for benchmarks216- Prefer stack allocation (arrays, small structs) over heap allocation where possible217218## Project Structure219```220src/221 main.rs or lib.rs222 config.rs223 error.rs # Custom error types224 models/ # Data structures225 handlers/ # Request handlers (for web servers)226 services/ # Business logic227 db/ # Database access228tests/229 integration/ # Integration tests230benches/ # Criterion benchmarks231```232233---234> Source: [bugrabilge/bilge-development-kit](https://github.com/bugrabilge/bilge-development-kit) — distributed by [TomeVault](https://tomevault.io).235<!-- tomevault:4.0:skill_md:2026-06-16 -->