rust-advanced
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.
Ownership & Borrowing Rules
Interior mutability — decision flowchart
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 |
|---|---|
Box<T> |
Recursive types, large stack values, trait objects |
Rc<T> |
Single-threaded shared ownership (trees, graphs) |
Arc<T> |
Multi-threaded shared ownership |
Cow<'a, T> |
Sometimes borrowed, sometimes owned — avoid eager clones |
Pin<Box<T>> |
Self-referential types, async futures |
The Cow rule
Accept Cow<str> or Cow<[T]> when a function sometimes modifies its input and
sometimes passes it through unchanged. This avoids allocating when no modification
is needed. Prefer &str in function arguments when you never need ownership.
Error Handling Strategy
The golden rule: libraries use thiserror, applications use anyhow
| Context | Crate | Why |
|---|---|---|
| Library crate | thiserror |
Callers need to match on specific error variants |
| Binary / application | anyhow |
Errors bubble up to user-facing messages with context |
| Internal modules | thiserror |
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:fs::read_to_string(path) .with_context(|| format!("failed to read config: {path}"))?; -
Use
#[from]for automatic conversions in library error enums:#[derive(thiserror::Error, Debug)] pub enum DbError { #[error("connection failed: {0}")] Connection(#[from] std::io::Error), #[error("query failed: {reason}")] Query { reason: String }, } -
Prefer
Resultcombinators over nestedmatchfor short chains:map,map_err,and_then,unwrap_or_else. -
Never
unwrap()in library code. Useexpect()only when the invariant is documented and provably upheld.
Trait System Conventions
Trait objects vs generics — decision rule
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.,
Iterator::Item). - 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 dyn Trait) only if:
- No methods return
Self - No methods have generic type parameters
- All methods take
self,&self, or&mut self
If you need dyn Trait + async, use #[async_trait] or return
Box<dyn Future> manually — native async in traits is not yet object-safe.
Async Rust Rules
Runtime: Tokio is the default
Use tokio with #[tokio::main] and #[tokio::test]. For CPU-bound work
inside an async context, use tokio::task::spawn_blocking or rayon.
Native async traits — drop #[async_trait] where possible
Since Rust 1.75, async fn in traits works natively. Use native syntax unless
you need dyn Trait with async methods.
The Send/Sync rule
Futures passed to tokio::spawn must be Send. The #1 cause of non-Send
futures: holding a MutexGuard (or any !Send type) across an .await point.
Fix: drop the guard before awaiting, or scope the lock in a block:
{
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 .await point (especially in tokio::select!).
Know which operations are cancel-safe:
| Operation | Cancel-safe? |
|---|---|
mpsc::Receiver::recv |
Yes |
AsyncReadExt::read |
Yes |
AsyncWriteExt::write_all |
No |
AsyncBufReadExt::read_line |
No |
For cancel-unsafe code: wrap in tokio::spawn (dropping a JoinHandle does not
cancel the spawned task) or use tokio_util::sync::CancellationToken for
cooperative cancellation.
Structured concurrency: use JoinSet
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 UserId with OrderId:
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:
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
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 |
|---|---|---|
PhantomData<T> |
Covariant | "Owns" a T conceptually |
PhantomData<fn(T)> |
Contravariant | Consumes T (rare) |
PhantomData<fn(T) -> T> |
Invariant | Must be exact type |
PhantomData<*const T> |
Invariant | Raw pointer semantics |
Performance Decision Framework
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 (filter().map().sum()) 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
unsafe {}. - Mandatory
// SAFETY:comment on everyunsafeblock explaining why the invariants are upheld. - Prefer safe abstractions —
ascasts,bytemuck::cast,from_raw_partsovertransmute. Usetransmuteonly as last resort with turbofish syntax. - FFI boundary rule: generate bindings with
bindgen, wrap in a thin safe Rust API, document every invariant. - Never use
unsafeto bypass the borrow checker. If you think you need to, redesign the data structure.
Common Pitfalls
-
Holding
MutexGuardacross.await— makes the future!Send, breakstokio::spawn. Scope the lock in a block before awaiting. -
RefCelldouble borrow panic —borrow_mut()panics if any borrow is live. Usetry_borrow_mut()when borrow lifetimes aren't fully controlled. -
Mutexdeadlock — Rust'sMutexis non-reentrant. Never lock the same mutex twice on one thread. Acquire multiple locks in consistent order. -
collect::<Vec<Result<T, E>>>()vscollect::<Result<Vec<T>, E>>()— the second form fails fast on first error and is almost always what you want. -
Accepting
&Stringinstead of&str—&Stringauto-derefs to&strbut not vice versa. Always accept&strin function signatures. -
unwrap()in library code — crashes the caller. Use?with proper error types, orexpect()with documented invariant. -
Forgetting
#[must_use]onResult-returning functions — callers may silently ignore errors. The compiler warns, but custom types need the attribute. -
Using
std::sync::Mutexin async code — blocks the executor thread. Usetokio::sync::Mutexfor async contexts. -
String::fromin hot loops — allocates each iteration. Pre-allocate withString::with_capacity()or useCow<str>. -
Ignoring cancellation safety in
select!— the non-winning future is dropped. Cancel-unsafe operations lose data silently. -
clone()as first instinct — usually a sign of fighting the borrow checker. Restructure ownership or use references first. -
Box<dyn Error>instead of proper error enum — loses the ability to match on specific variants. Usethiserrorfor structured errors.
Reference Files
Read the relevant reference file when working with a specific topic:
| File | When to read |
|---|---|
references/ownership.md |
Interior mutability, smart pointers, Cow, Pin, lifetime tricks |
references/traits.md |
Trait objects, sealed traits, blanket impls, HRTB, variance |
references/error-handling.md |
thiserror v2, anyhow, Result combinators, error design |
references/async-rust.md |
Tokio runtime, cancellation, JoinSet, Send/Sync, select! |
references/performance.md |
Zero-cost, SIMD, arena allocation, rayon, cache optimization |
references/unsafe-ffi.md |
Unsafe superpowers, FFI with bindgen, transmute, raw pointers |
references/macros.md |
Declarative macros, proc macros, derive macros, syn/quote |
references/type-patterns.md |
Newtype, typestate, PhantomData, const generics, builder |
More from trancong12102/agentskills
deps-dev
Look up the latest stable version of any open-source package across npm, PyPI, Go, Cargo, Maven, and NuGet. Use when the user asks 'what's the latest version of X', 'what version should I use', 'is X deprecated', 'how outdated is my package.json/requirements.txt/Cargo.toml', or needs version numbers for adding or updating dependencies. Also covers pinning versions, checking if packages are maintained, or comparing installed vs latest versions. Do NOT use for private/internal packages or for looking up documentation (use context7).
151council-review
Multi-perspective code review that synthesizes findings from multiple reviewers into a unified report. Use when the user asks to review code changes, audit a diff, check code quality, review a PR, review commits, or review uncommitted changes. Also covers 'code review', 'review my changes', 'check this before I merge', or wanting multiple perspectives on code. Do not use for documentation/markdown review or trivial single-line changes.
94oracle
Deep analysis and expert reasoning. Use when the user asks for 'oracle', 'second opinion', architecture analysis, elusive bug debugging, impact assessment, security reasoning, refactoring strategy, or trade-off evaluation — problems that benefit from deep, independent reasoning. Do not use for simple factual questions, code generation, code review (use council-review), or tasks needing file modifications.
92context7
Fetch up-to-date documentation for any open-source library or framework. Use when the user asks to look up docs, check an API, find code examples, or verify how a feature works — especially with a specific library name, version migration, or phrases like 'what's the current way to...' or 'the API might have changed'. Also covers setup and configuration docs. Do NOT use for general programming concepts, internal project code, or version lookups (use deps-dev).
86conventional-commit
Generates git commit messages following Conventional Commits 1.0.0 specification with semantic types (feat, fix, etc.), optional scope, and breaking change annotations. Use when committing code changes or creating commit messages.
58react-web-advanced
Web-specific React patterns for type-safe file-based routing, route-level data loading, server-side rendering, search param validation, code splitting, and list virtualization. Use when building React web apps with route loaders, SSR streaming, validated search params, lazy route splitting, or virtualizing large DOM lists. Do not use for React Native apps — use react-native-advanced instead.
45