Rust Advanced: Patterns, Conventions & Pitfalls
This skill defines rules, conventions, and architectural decisions for building
production Rust applications. It is intentionally opinionated to prevent common
pitfalls and enforce patterns that scale.
For detailed API documentation of any crate mentioned here, use other appropriate
tools (documentation lookup, web search, etc.) — this skill focuses on how and
why to use these patterns, not full API surfaces.
Table of Contents
- Ownership & Borrowing Rules
- Error Handling Strategy
- Trait System Conventions
- Async Rust Rules
- Type System Patterns
- Performance Decision Framework
- Unsafe Policy
- Common Pitfalls
- Reference Files
Ownership & Borrowing Rules
Interior mutability — decision flowchart
text
Need shared mutation?
YES → Single-threaded or multi-threaded?
Single-threaded → Is T: Copy?
YES → Cell<T> (zero overhead, no borrow tracking)
NO → RefCell<T> (runtime borrow checking, panics on violation)
Multi-threaded → High contention?
NO → Arc<Mutex<T>> (simple, correct)
YES → Arc<RwLock<T>> (many readers, few writers)
or lock-free types (crossbeam, atomic)
NO → Use normal ownership / borrowing
Smart pointer selection
| Type | When to use |
|---|
| Recursive types, large stack values, trait objects |
| Single-threaded shared ownership (trees, graphs) |
| Multi-threaded shared ownership |
| Sometimes borrowed, sometimes owned — avoid eager clones |
| Self-referential types, async futures |
The Cow rule
Accept
or
when a function sometimes modifies its input and
sometimes passes it through unchanged. This avoids allocating when no modification
is needed. Prefer
in function arguments when you never need ownership.
Error Handling Strategy
The golden rule: libraries use , applications use
| Context | Crate | Why |
|---|
| Library crate | | Callers need to match on specific error variants |
| Binary / application | | Errors bubble up to user-facing messages with context |
| Internal modules | | Type-safe error variants for the parent module to handle |
| FFI boundary | Custom enum | Must map to C-compatible error codes |
Required patterns
-
Always add context when propagating with
in application code:
rust
fs::read_to_string(path)
.with_context(|| format!("failed to read config: {path}"))?;
-
Use for automatic conversions in library error enums:
rust
#[derive(thiserror::Error, Debug)]
pub enum DbError {
#[error("connection failed: {0}")]
Connection(#[from] std::io::Error),
#[error("query failed: {reason}")]
Query { reason: String },
}
-
Prefer combinators over nested
for short chains:
,
,
,
.
-
Never in library code. Use
only when the invariant
is documented and provably upheld.
Trait System Conventions
Trait objects vs generics — decision rule
text
Need runtime polymorphism (heterogeneous collection, plugin system)?
YES → dyn Trait (Box<dyn Trait> or &dyn Trait)
NO → impl Trait / generics (zero-cost, monomorphized)
Key patterns
- Associated types over generics when there is exactly one natural
implementation per type (e.g., ).
- Sealed traits when you need to prevent downstream crates from implementing
your trait — essential for semver stability.
- Blanket implementations to extend functionality to all types satisfying a
bound (e.g.,
impl<T: Display> ToString for T
).
- Supertraits when your trait logically requires another trait's guarantees
(e.g.,
trait Printable: Debug + Display
).
Object safety rules
A trait is object-safe (can be used as
) only if:
- No methods return
- No methods have generic type parameters
- All methods take , , or
If you need
, use
or return
manually — native async in traits is not yet object-safe.
Async Rust Rules
Runtime: Tokio is the default
Use
with
and
. For CPU-bound work
inside an async context, use
tokio::task::spawn_blocking
or
.
Native async traits — drop where possible
Since Rust 1.75,
in traits works natively. Use native syntax unless
you need
with async methods.
The Send/Sync rule
Futures passed to
must be
. The #1 cause of non-Send
futures: holding a
(or any
type) across an
point.
Fix: drop the guard before awaiting, or scope the lock in a block:
rust
{
let mut guard = lock.lock().unwrap();
guard.push(42);
} // guard dropped
do_async_thing().await; // future is Send
Cancellation safety — the most dangerous async footgun
Any future can be dropped at any
point (especially in
).
Know which operations are cancel-safe:
| Operation | Cancel-safe? |
|---|
| Yes |
| Yes |
| No |
AsyncBufReadExt::read_line
| No |
For cancel-unsafe code: wrap in
(dropping a
does not
cancel the spawned task) or use
tokio_util::sync::CancellationToken
for
cooperative cancellation.
Structured concurrency: use
rust
let mut set = tokio::task::JoinSet::new();
for url in urls {
set.spawn(fetch(url));
}
while let Some(result) = set.join_next().await {
result??;
}
Type System Patterns
Newtype — zero-cost domain types
Wrap primitives to create distinct types. Prevents mixing
with
:
rust
struct UserId(u64);
struct OrderId(u64);
// fn process(user: UserId, order: OrderId) — compiler prevents swaps
Typestate — compile-time state machine
Encode lifecycle states as type parameters. Invalid transitions become compile errors:
rust
struct Connection<S> { socket: TcpStream, _state: PhantomData<S> }
struct Disconnected;
struct Connected;
impl Connection<Disconnected> {
fn connect(self) -> Result<Connection<Connected>> { ... }
}
impl Connection<Connected> {
fn send(&self, data: &[u8]) -> Result<()> { ... }
// send() is unavailable on Connection<Disconnected>
}
Const generics — array sizes as type parameters
rust
struct Matrix<const ROWS: usize, const COLS: usize> {
data: [[f64; COLS]; ROWS],
}
impl<const N: usize> Matrix<N, N> {
fn trace(&self) -> f64 { (0..N).map(|i| self.data[i][i]).sum() }
}
PhantomData variance
| Marker | Variance | Use for |
|---|
| Covariant | "Owns" a T conceptually |
| Contravariant | Consumes T (rare) |
| Invariant | Must be exact type |
| Invariant | Raw pointer semantics |
Performance Decision Framework
text
Is this a hot path (profiled, not guessed)?
NO → Write clear, idiomatic code. Don't optimize.
YES → Which bottleneck?
CPU-bound computation → rayon::par_iter() for data parallelism
Many small allocations → Arena allocator (bumpalo)
Iterator chain not vectorizing → Check for stateful dependencies,
use fold/try_fold, or restructure as plain slice iteration
Cache misses → #[repr(C)] + align, struct-of-arrays layout
Heap allocation → Box<[T]> instead of Vec<T> when size is fixed,
stack allocation for small types, SmallVec for usually-small vecs
The zero-cost rule
Iterator chains (
) compile to the same code as hand-written
loops — prefer them for readability. But stateful iterator chains can block
auto-vectorization; see
references/performance.md
for SIMD details.
Unsafe Policy
- Minimize scope — wrap only the minimum number of lines in .
- Mandatory comment on every block explaining
why the invariants are upheld.
- Prefer safe abstractions — casts, ,
over . Use only as last resort with turbofish syntax.
- FFI boundary rule: generate bindings with , wrap in a thin safe
Rust API, document every invariant.
- Never use to bypass the borrow checker. If you think you need to,
redesign the data structure.
Common Pitfalls
-
Holding across — makes the future
, breaks
. Scope the lock in a block before awaiting.
-
double borrow panic —
panics if any borrow is
live. Use
when borrow lifetimes aren't fully controlled.
-
deadlock — Rust's
is non-reentrant. Never lock the same
mutex twice on one thread. Acquire multiple locks in consistent order.
-
collect::<Vec<Result<T, E>>>()
vs collect::<Result<Vec<T>, E>>()
—
the second form fails fast on first error and is almost always what you want.
-
Accepting instead of —
auto-derefs to
but not vice versa. Always accept
in function signatures.
-
in library code — crashes the caller. Use
with proper
error types, or
with documented invariant.
-
Forgetting on -returning functions — callers may
silently ignore errors. The compiler warns, but custom types need the attribute.
-
Using in async code — blocks the executor thread.
Use
for async contexts.
-
in hot loops — allocates each iteration. Pre-allocate
with
or use
.
-
Ignoring cancellation safety in — the non-winning future is
dropped. Cancel-unsafe operations lose data silently.
-
as first instinct — usually a sign of fighting the borrow
checker. Restructure ownership or use references first.
-
instead of proper error enum — loses the ability to
match on specific variants. Use
for structured errors.
Reference Files
Read the relevant reference file when working with a specific topic:
| File | When to read |
|---|
| Interior mutability, smart pointers, Cow, Pin, lifetime tricks |
| Trait objects, sealed traits, blanket impls, HRTB, variance |
references/error-handling.md
| thiserror v2, anyhow, Result combinators, error design |
| Tokio runtime, cancellation, JoinSet, Send/Sync, select! |
references/performance.md
| Zero-cost, SIMD, arena allocation, rayon, cache optimization |
| Unsafe superpowers, FFI with bindgen, transmute, raw pointers |
| Declarative macros, proc macros, derive macros, syn/quote |
references/type-patterns.md
| Newtype, typestate, PhantomData, const generics, builder |