Rust Operations
Comprehensive Rust skill covering ownership, async, error handling, and the production ecosystem.
Ecosystem facts verified as of 2026-07.
Staleness check: python scripts/check-rust-facts.py --offline asserts the
catalogued version-bearing facts (tokio, axum, serde) are still named in the prose
and the dated currency note above is present; run --live to confirm each crate's
crates.io major still matches the documented major. Catalog: assets/rust-facts.json.
Ownership Quick Reference
Who owns the value?
│
├─ Need to transfer ownership
│ └─ Move: let s2 = s1; (s1 is invalid after this)
│
├─ Need to read without owning
│ └─ Shared borrow: &T (multiple allowed, no mutation)
│
├─ Need to mutate without owning
│ └─ Exclusive borrow: &mut T (only one, no other borrows)
│
├─ Need to share ownership across threads
│ └─ Arc<T> (atomic reference counting)
│ └─ Need mutation too? Arc<Mutex<T>>
│
├─ Need to share ownership single-threaded
│ └─ Rc<T> (reference counting, not Send)
│ └─ Need mutation too? Rc<RefCell<T>>
│
└─ Need to avoid cloning large data
└─ Cow<'a, T> (clone-on-write, borrows when possible)
The Borrow Rules
- At any time, you can have either one
&mut T or any number of &T
- References must always be valid (no dangling)
- These rules are enforced at compile time (zero runtime cost)
Error Handling Decision Tree
What kind of error?
│
├─ Operation might not have a value (no error info needed)
│ └─ Option<T>: Some(value) or None
│
├─ Library code (callers need to match on error variants)
│ └─ thiserror: #[derive(Error)] enum with variants
│ └─ Each variant can wrap source errors with #[from]
│
├─ Application code (just need context, not matching)
│ └─ anyhow: anyhow::Result<T>, .context("msg")
│
├─ Converting between error types
│ └─ impl From<SourceError> for MyError
│ └─ Or use #[from] with thiserror
│
└─ Truly unrecoverable (violating invariants)
└─ panic!() or unwrap() - avoid in library code
thiserror (Library Errors)
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("not found: {entity} with id {id}")]
NotFound { entity: &'static str, id: i64 },
#[error("validation failed: {0}")]
Validation(String),
}
anyhow (Application Errors)
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.context("failed to read config file")?;
let config: Config = toml::from_str(&content)
.context("failed to parse config")?;
Ok(config)
}
The ? Operator
// ? on Result: returns Err early, unwraps Ok
let file = File::open(path)?;
// ? on Option: returns None early, unwraps Some
let first = items.first()?;
// Chain with map_err for context
let port: u16 = env::var("PORT")
.map_err(|_| AppError::Config("PORT not set"))?
.parse()
.map_err(|_| AppError::Config("PORT not a number"))?;
Deep dive: Load ./references/error-handling.md for Result/Option combinators, error conversion patterns, panic/recover.
Trait Design Quick Reference
Common Derives
#[derive(Debug, Clone, PartialEq, Eq, Hash)] // Value types
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] // API types
#[derive(Debug, thiserror::Error)] // Error types
Trait Objects vs Generics
|
Trait Objects (dyn Trait) |
Generics (T: Trait) |
| Dispatch |
Dynamic (vtable) |
Static (monomorphized) |
| Binary size |
Smaller |
Larger (per-type copies) |
| Performance |
Slight overhead |
Zero-cost |
| Heterogeneous collections |
Yes |
No |
| Use when |
Runtime polymorphism, plugin systems |
Performance-critical, known types |
// Generics (preferred when types known at compile time)
fn process<T: Display>(item: T) { println!("{item}"); }
// Trait objects (when you need heterogeneous collections)
fn process_all(items: &[Box<dyn Display>]) {
for item in items { println!("{item}"); }
}
Key Traits to Know
| Trait |
Purpose |
Auto-derive? |
Debug |
Debug formatting |
Yes |
Clone |
Explicit copy |
Yes |
Copy |
Implicit copy (small, stack-only) |
Yes |
Display |
User-facing formatting |
No (impl manually) |
From/Into |
Type conversion |
No (impl From, get Into free) |
Send |
Safe to send between threads |
Auto |
Sync |
Safe to share references between threads |
Auto |
Deref |
Smart pointer dereference |
No |
Iterator |
Iteration protocol |
No |
Default |
Default value |
Yes |
Deep dive: Load ./references/traits-generics.md for associated types, supertraits, sealed traits, extension traits.
Async Decision Tree
Do you need async?
│
├─ I/O-heavy (network, files, databases)
│ └─ Yes. Use tokio.
│
├─ CPU-heavy computation
│ └─ No. Use rayon for data parallelism.
│ └─ Or tokio::task::spawn_blocking for mixing with async
│
├─ Simple scripts or CLI tools
│ └─ Probably not. Blocking I/O is fine.
│
└─ Yes, I need async:
│
├─ Runtime: tokio (dominant), or async-std
├─ HTTP client: reqwest
├─ HTTP server: axum (tower-based) or actix-web
├─ Database: sqlx (compile-time checked)
└─ Structured logging: tracing
tokio Quick Start
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Spawn concurrent tasks
let (a, b) = tokio::join!(
fetch_users(),
fetch_orders(),
);
// Select first to complete
tokio::select! {
result = long_operation() => handle(result),
_ = tokio::time::sleep(Duration::from_secs(5)) => {
eprintln!("timeout");
}
}
Ok(())
}
Channel Types
| Channel |
Use Case |
Import |
mpsc |
Multiple producers, single consumer |
tokio::sync::mpsc |
oneshot |
Single value, single use |
tokio::sync::oneshot |
broadcast |
Multiple consumers, all get every message |
tokio::sync::broadcast |
watch |
Single value, latest-only (config reload) |
tokio::sync::watch |
Deep dive: Load ./references/async-tokio.md for spawn patterns, graceful shutdown, Mutex choice, async traits, streams.
Cargo Quick Reference
# Create project
cargo new my-project # binary
cargo new my-lib --lib # library
# Build and run
cargo build # debug
cargo build --release # optimized
cargo run -- args # build + run
cargo run --example name # run example
# Test
cargo test # all tests
cargo test test_name # specific test
cargo test -- --nocapture # show println output
# Dependencies
cargo add serde --features derive # add dep
cargo add tokio -F full # shorthand
cargo update # update lock file
# Check without building
cargo check # fast type checking
cargo clippy # lints
cargo fmt # format
# Workspace
cargo test --workspace # test all crates
cargo build -p my-crate # build specific crate
Feature Flags
[features]
default = ["json"]
json = ["dep:serde_json"]
full = ["json", "yaml", "toml"]
[dependencies]
serde_json = { version = "1", optional = true }
Release Profile Tuning
[profile.release]
lto = true # Link-time optimization: smaller, faster binaries
codegen-units = 1 # Better optimization at the cost of compile time
Common Gotchas
| Gotcha |
Why |
Fix |
String vs &str |
Owned vs borrowed, function signatures |
Accept &str in params, return String |
| Borrow checker fight |
Borrowing self while mutating |
Split struct, use indices, clone (if cheap) |
| Lifetime elision confusion |
Hidden lifetimes in function signatures |
Write them out explicitly to understand, then elide |
impl Trait in return |
Different branches must return same type |
Use Box<dyn Trait> for heterogeneous returns |
tokio::Mutex vs std::Mutex |
std::Mutex can't be held across .await |
Use tokio::Mutex across await points |
| Orphan rule |
Can't impl foreign trait for foreign type |
Newtype pattern: struct Wrapper(ForeignType) |
Pin confusion |
Required for self-referential async futures |
Use Box::pin(), don't fight it |
Send bounds on async |
Spawned futures must be Send |
Avoid Rc, RefCell in async; use Arc, Mutex |
.unwrap() in production |
Panics on None/Err |
Use ?, .unwrap_or(), .expect("reason") |
serde Quick Reference
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct User {
user_id: i64,
display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
#[serde(default)]
is_active: bool,
#[serde(rename = "type")]
user_type: UserType,
#[serde(with = "chrono::serde::ts_seconds")]
created_at: DateTime<Utc>,
}
// Serialize
let json = serde_json::to_string(&user)?;
let yaml = serde_yaml::to_string(&user)?;
// Deserialize
let user: User = serde_json::from_str(&json)?;
Deep dive: Load ./references/ecosystem.md for serde advanced usage, clap, reqwest, sqlx, axum, tracing, rayon.
Reference Files
Load these for deep-dive topics. Each is self-contained.
| Reference |
When to Load |
./references/ownership-lifetimes.md |
Borrowing rules, lifetime annotations, elision, interior mutability, common borrow checker patterns |
./references/traits-generics.md |
Trait design, associated types, supertraits, generics, constraints, sealed/extension traits |
./references/error-handling.md |
Result/Option combinators, thiserror/anyhow deep dive, error conversion, panic/recover |
./references/async-tokio.md |
tokio runtime, spawn, channels, select, streams, graceful shutdown, async traits, Mutex choice |
./references/ecosystem.md |
serde advanced, clap, reqwest, sqlx, axum, tracing, rayon, itertools, Cow |
./references/testing.md |
Unit/integration/doc tests, async tests, mockall, proptest, criterion benchmarks |
See Also
docker-ops - Multi-stage builds for Rust (scratch/distroless, cargo-chef for layer caching)
ci-cd-ops - Rust CI pipelines, cargo caching, cross-compilation
testing-ops - Cross-language testing strategies
1---2name: rust-ops3description: "Rust development patterns, ownership, async, error handling, and ecosystem. Use for: rust, cargo, ownership, borrow checker, lifetime, tokio, serde, trait, Result, Option, async rust, crate, derive, impl, enum, pattern matching, Arc, Mutex, Send, Sync, thiserror, anyhow, clap, axum, sqlx, reqwest, rayon, tracing."4license: MIT5allowed-tools: "Read Write Bash"6metadata:7 author: claude-mods8 related-skills: docker-ops, ci-cd-ops, testing-ops9---1011# Rust Operations1213Comprehensive Rust skill covering ownership, async, error handling, and the production ecosystem.1415> Ecosystem facts verified as of 2026-07.1617**Staleness check:** `python scripts/check-rust-facts.py --offline` asserts the18catalogued version-bearing facts (tokio, axum, serde) are still named in the prose19and the dated currency note above is present; run `--live` to confirm each crate's20crates.io major still matches the documented major. Catalog: `assets/rust-facts.json`.2122## Ownership Quick Reference2324```25Who owns the value?26│27├─ Need to transfer ownership28│ └─ Move: let s2 = s1; (s1 is invalid after this)29│30├─ Need to read without owning31│ └─ Shared borrow: &T (multiple allowed, no mutation)32│33├─ Need to mutate without owning34│ └─ Exclusive borrow: &mut T (only one, no other borrows)35│36├─ Need to share ownership across threads37│ └─ Arc<T> (atomic reference counting)38│ └─ Need mutation too? Arc<Mutex<T>>39│40├─ Need to share ownership single-threaded41│ └─ Rc<T> (reference counting, not Send)42│ └─ Need mutation too? Rc<RefCell<T>>43│44└─ Need to avoid cloning large data45 └─ Cow<'a, T> (clone-on-write, borrows when possible)46```4748### The Borrow Rules49501. At any time, you can have **either** one `&mut T` **or** any number of `&T`512. References must always be valid (no dangling)523. These rules are enforced at compile time (zero runtime cost)5354## Error Handling Decision Tree5556```57What kind of error?58│59├─ Operation might not have a value (no error info needed)60│ └─ Option<T>: Some(value) or None61│62├─ Library code (callers need to match on error variants)63│ └─ thiserror: #[derive(Error)] enum with variants64│ └─ Each variant can wrap source errors with #[from]65│66├─ Application code (just need context, not matching)67│ └─ anyhow: anyhow::Result<T>, .context("msg")68│69├─ Converting between error types70│ └─ impl From<SourceError> for MyError71│ └─ Or use #[from] with thiserror72│73└─ Truly unrecoverable (violating invariants)74 └─ panic!() or unwrap() - avoid in library code75```7677### thiserror (Library Errors)7879```rust80use thiserror::Error;8182#[derive(Debug, Error)]83pub enum AppError {84 #[error("database error: {0}")]85 Database(#[from] sqlx::Error),8687 #[error("not found: {entity} with id {id}")]88 NotFound { entity: &'static str, id: i64 },8990 #[error("validation failed: {0}")]91 Validation(String),92}93```9495### anyhow (Application Errors)9697```rust98use anyhow::{Context, Result};99100fn load_config(path: &str) -> Result<Config> {101 let content = std::fs::read_to_string(path)102 .context("failed to read config file")?;103 let config: Config = toml::from_str(&content)104 .context("failed to parse config")?;105 Ok(config)106}107```108109### The ? Operator110111```rust112// ? on Result: returns Err early, unwraps Ok113let file = File::open(path)?;114115// ? on Option: returns None early, unwraps Some116let first = items.first()?;117118// Chain with map_err for context119let port: u16 = env::var("PORT")120 .map_err(|_| AppError::Config("PORT not set"))?121 .parse()122 .map_err(|_| AppError::Config("PORT not a number"))?;123```124125**Deep dive**: Load `./references/error-handling.md` for Result/Option combinators, error conversion patterns, panic/recover.126127## Trait Design Quick Reference128129### Common Derives130131```rust132#[derive(Debug, Clone, PartialEq, Eq, Hash)] // Value types133#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] // API types134#[derive(Debug, thiserror::Error)] // Error types135```136137### Trait Objects vs Generics138139| | Trait Objects (`dyn Trait`) | Generics (`T: Trait`) |140|---|---|---|141| Dispatch | Dynamic (vtable) | Static (monomorphized) |142| Binary size | Smaller | Larger (per-type copies) |143| Performance | Slight overhead | Zero-cost |144| Heterogeneous collections | Yes | No |145| Use when | Runtime polymorphism, plugin systems | Performance-critical, known types |146147```rust148// Generics (preferred when types known at compile time)149fn process<T: Display>(item: T) { println!("{item}"); }150151// Trait objects (when you need heterogeneous collections)152fn process_all(items: &[Box<dyn Display>]) {153 for item in items { println!("{item}"); }154}155```156157### Key Traits to Know158159| Trait | Purpose | Auto-derive? |160|-------|---------|-------------|161| `Debug` | Debug formatting | Yes |162| `Clone` | Explicit copy | Yes |163| `Copy` | Implicit copy (small, stack-only) | Yes |164| `Display` | User-facing formatting | No (impl manually) |165| `From`/`Into` | Type conversion | No (impl `From`, get `Into` free) |166| `Send` | Safe to send between threads | Auto |167| `Sync` | Safe to share references between threads | Auto |168| `Deref` | Smart pointer dereference | No |169| `Iterator` | Iteration protocol | No |170| `Default` | Default value | Yes |171172**Deep dive**: Load `./references/traits-generics.md` for associated types, supertraits, sealed traits, extension traits.173174## Async Decision Tree175176```177Do you need async?178│179├─ I/O-heavy (network, files, databases)180│ └─ Yes. Use tokio.181│182├─ CPU-heavy computation183│ └─ No. Use rayon for data parallelism.184│ └─ Or tokio::task::spawn_blocking for mixing with async185│186├─ Simple scripts or CLI tools187│ └─ Probably not. Blocking I/O is fine.188│189└─ Yes, I need async:190 │191 ├─ Runtime: tokio (dominant), or async-std192 ├─ HTTP client: reqwest193 ├─ HTTP server: axum (tower-based) or actix-web194 ├─ Database: sqlx (compile-time checked)195 └─ Structured logging: tracing196```197198### tokio Quick Start199200```rust201#[tokio::main]202async fn main() -> anyhow::Result<()> {203 // Spawn concurrent tasks204 let (a, b) = tokio::join!(205 fetch_users(),206 fetch_orders(),207 );208209 // Select first to complete210 tokio::select! {211 result = long_operation() => handle(result),212 _ = tokio::time::sleep(Duration::from_secs(5)) => {213 eprintln!("timeout");214 }215 }216217 Ok(())218}219```220221### Channel Types222223| Channel | Use Case | Import |224|---------|----------|--------|225| `mpsc` | Multiple producers, single consumer | `tokio::sync::mpsc` |226| `oneshot` | Single value, single use | `tokio::sync::oneshot` |227| `broadcast` | Multiple consumers, all get every message | `tokio::sync::broadcast` |228| `watch` | Single value, latest-only (config reload) | `tokio::sync::watch` |229230**Deep dive**: Load `./references/async-tokio.md` for spawn patterns, graceful shutdown, Mutex choice, async traits, streams.231232## Cargo Quick Reference233234```bash235# Create project236cargo new my-project # binary237cargo new my-lib --lib # library238239# Build and run240cargo build # debug241cargo build --release # optimized242cargo run -- args # build + run243cargo run --example name # run example244245# Test246cargo test # all tests247cargo test test_name # specific test248cargo test -- --nocapture # show println output249250# Dependencies251cargo add serde --features derive # add dep252cargo add tokio -F full # shorthand253cargo update # update lock file254255# Check without building256cargo check # fast type checking257cargo clippy # lints258cargo fmt # format259260# Workspace261cargo test --workspace # test all crates262cargo build -p my-crate # build specific crate263```264265### Feature Flags266267```toml268[features]269default = ["json"]270json = ["dep:serde_json"]271full = ["json", "yaml", "toml"]272273[dependencies]274serde_json = { version = "1", optional = true }275```276277### Release Profile Tuning278279```toml280[profile.release]281lto = true # Link-time optimization: smaller, faster binaries282codegen-units = 1 # Better optimization at the cost of compile time283```284285## Common Gotchas286287| Gotcha | Why | Fix |288|--------|-----|-----|289| `String` vs `&str` | Owned vs borrowed, function signatures | Accept `&str` in params, return `String` |290| Borrow checker fight | Borrowing self while mutating | Split struct, use indices, clone (if cheap) |291| Lifetime elision confusion | Hidden lifetimes in function signatures | Write them out explicitly to understand, then elide |292| `impl Trait` in return | Different branches must return same type | Use `Box<dyn Trait>` for heterogeneous returns |293| `tokio::Mutex` vs `std::Mutex` | `std::Mutex` can't be held across `.await` | Use `tokio::Mutex` across await points |294| Orphan rule | Can't impl foreign trait for foreign type | Newtype pattern: `struct Wrapper(ForeignType)` |295| `Pin` confusion | Required for self-referential async futures | Use `Box::pin()`, don't fight it |296| `Send` bounds on async | Spawned futures must be `Send` | Avoid `Rc`, `RefCell` in async; use `Arc`, `Mutex` |297| `.unwrap()` in production | Panics on None/Err | Use `?`, `.unwrap_or()`, `.expect("reason")` |298299## serde Quick Reference300301```rust302use serde::{Serialize, Deserialize};303304#[derive(Serialize, Deserialize)]305#[serde(rename_all = "camelCase")]306struct User {307 user_id: i64,308 display_name: String,309310 #[serde(skip_serializing_if = "Option::is_none")]311 email: Option<String>,312313 #[serde(default)]314 is_active: bool,315316 #[serde(rename = "type")]317 user_type: UserType,318319 #[serde(with = "chrono::serde::ts_seconds")]320 created_at: DateTime<Utc>,321}322323// Serialize324let json = serde_json::to_string(&user)?;325let yaml = serde_yaml::to_string(&user)?;326327// Deserialize328let user: User = serde_json::from_str(&json)?;329```330331**Deep dive**: Load `./references/ecosystem.md` for serde advanced usage, clap, reqwest, sqlx, axum, tracing, rayon.332333## Reference Files334335Load these for deep-dive topics. Each is self-contained.336337| Reference | When to Load |338|-----------|-------------|339| `./references/ownership-lifetimes.md` | Borrowing rules, lifetime annotations, elision, interior mutability, common borrow checker patterns |340| `./references/traits-generics.md` | Trait design, associated types, supertraits, generics, constraints, sealed/extension traits |341| `./references/error-handling.md` | Result/Option combinators, thiserror/anyhow deep dive, error conversion, panic/recover |342| `./references/async-tokio.md` | tokio runtime, spawn, channels, select, streams, graceful shutdown, async traits, Mutex choice |343| `./references/ecosystem.md` | serde advanced, clap, reqwest, sqlx, axum, tracing, rayon, itertools, Cow |344| `./references/testing.md` | Unit/integration/doc tests, async tests, mockall, proptest, criterion benchmarks |345346## See Also347348- `docker-ops` - Multi-stage builds for Rust (scratch/distroless, cargo-chef for layer caching)349- `ci-cd-ops` - Rust CI pipelines, cargo caching, cross-compilation350- `testing-ops` - Cross-language testing strategies