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: MIT5---67# Rust Operations89Comprehensive Rust skill covering ownership, async, error handling, and the production ecosystem.1011> Ecosystem facts verified as of 2026-07.1213**Staleness check:** `python scripts/check-rust-facts.py --offline` asserts the14catalogued version-bearing facts (tokio, axum, serde) are still named in the prose15and the dated currency note above is present; run `--live` to confirm each crate's16crates.io major still matches the documented major. Catalog: `assets/rust-facts.json`.1718## Ownership Quick Reference1920```21Who owns the value?22│23├─ Need to transfer ownership24│ └─ Move: let s2 = s1; (s1 is invalid after this)25│26├─ Need to read without owning27│ └─ Shared borrow: &T (multiple allowed, no mutation)28│29├─ Need to mutate without owning30│ └─ Exclusive borrow: &mut T (only one, no other borrows)31│32├─ Need to share ownership across threads33│ └─ Arc<T> (atomic reference counting)34│ └─ Need mutation too? Arc<Mutex<T>>35│36├─ Need to share ownership single-threaded37│ └─ Rc<T> (reference counting, not Send)38│ └─ Need mutation too? Rc<RefCell<T>>39│40└─ Need to avoid cloning large data41 └─ Cow<'a, T> (clone-on-write, borrows when possible)42```4344### The Borrow Rules45461. At any time, you can have **either** one `&mut T` **or** any number of `&T`472. References must always be valid (no dangling)483. These rules are enforced at compile time (zero runtime cost)4950## Error Handling Decision Tree5152```53What kind of error?54│55├─ Operation might not have a value (no error info needed)56│ └─ Option<T>: Some(value) or None57│58├─ Library code (callers need to match on error variants)59│ └─ thiserror: #[derive(Error)] enum with variants60│ └─ Each variant can wrap source errors with #[from]61│62├─ Application code (just need context, not matching)63│ └─ anyhow: anyhow::Result<T>, .context("msg")64│65├─ Converting between error types66│ └─ impl From<SourceError> for MyError67│ └─ Or use #[from] with thiserror68│69└─ Truly unrecoverable (violating invariants)70 └─ panic!() or unwrap() - avoid in library code71```7273### thiserror (Library Errors)7475```rust76use thiserror::Error;7778#[derive(Debug, Error)]79pub enum AppError {80 #[error("database error: {0}")]81 Database(#[from] sqlx::Error),8283 #[error("not found: {entity} with id {id}")]84 NotFound { entity: &'static str, id: i64 },8586 #[error("validation failed: {0}")]87 Validation(String),88}89```9091### anyhow (Application Errors)9293```rust94use anyhow::{Context, Result};9596fn load_config(path: &str) -> Result<Config> {97 let content = std::fs::read_to_string(path)98 .context("failed to read config file")?;99 let config: Config = toml::from_str(&content)100 .context("failed to parse config")?;101 Ok(config)102}103```104105### The ? Operator106107```rust108// ? on Result: returns Err early, unwraps Ok109let file = File::open(path)?;110111// ? on Option: returns None early, unwraps Some112let first = items.first()?;113114// Chain with map_err for context115let port: u16 = env::var("PORT")116 .map_err(|_| AppError::Config("PORT not set"))?117 .parse()118 .map_err(|_| AppError::Config("PORT not a number"))?;119```120121**Deep dive**: Load `./references/error-handling.md` for Result/Option combinators, error conversion patterns, panic/recover.122123## Trait Design Quick Reference124125### Common Derives126127```rust128#[derive(Debug, Clone, PartialEq, Eq, Hash)] // Value types129#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] // API types130#[derive(Debug, thiserror::Error)] // Error types131```132133### Trait Objects vs Generics134135| | Trait Objects (`dyn Trait`) | Generics (`T: Trait`) |136|---|---|---|137| Dispatch | Dynamic (vtable) | Static (monomorphized) |138| Binary size | Smaller | Larger (per-type copies) |139| Performance | Slight overhead | Zero-cost |140| Heterogeneous collections | Yes | No |141| Use when | Runtime polymorphism, plugin systems | Performance-critical, known types |142143```rust144// Generics (preferred when types known at compile time)145fn process<T: Display>(item: T) { println!("{item}"); }146147// Trait objects (when you need heterogeneous collections)148fn process_all(items: &[Box<dyn Display>]) {149 for item in items { println!("{item}"); }150}151```152153### Key Traits to Know154155| Trait | Purpose | Auto-derive? |156|-------|---------|-------------|157| `Debug` | Debug formatting | Yes |158| `Clone` | Explicit copy | Yes |159| `Copy` | Implicit copy (small, stack-only) | Yes |160| `Display` | User-facing formatting | No (impl manually) |161| `From`/`Into` | Type conversion | No (impl `From`, get `Into` free) |162| `Send` | Safe to send between threads | Auto |163| `Sync` | Safe to share references between threads | Auto |164| `Deref` | Smart pointer dereference | No |165| `Iterator` | Iteration protocol | No |166| `Default` | Default value | Yes |167168**Deep dive**: Load `./references/traits-generics.md` for associated types, supertraits, sealed traits, extension traits.169170## Async Decision Tree171172```173Do you need async?174│175├─ I/O-heavy (network, files, databases)176│ └─ Yes. Use tokio.177│178├─ CPU-heavy computation179│ └─ No. Use rayon for data parallelism.180│ └─ Or tokio::task::spawn_blocking for mixing with async181│182├─ Simple scripts or CLI tools183│ └─ Probably not. Blocking I/O is fine.184│185└─ Yes, I need async:186 │187 ├─ Runtime: tokio (dominant), or async-std188 ├─ HTTP client: reqwest189 ├─ HTTP server: axum (tower-based) or actix-web190 ├─ Database: sqlx (compile-time checked)191 └─ Structured logging: tracing192```193194### tokio Quick Start195196```rust197#[tokio::main]198async fn main() -> anyhow::Result<()> {199 // Spawn concurrent tasks200 let (a, b) = tokio::join!(201 fetch_users(),202 fetch_orders(),203 );204205 // Select first to complete206 tokio::select! {207 result = long_operation() => handle(result),208 _ = tokio::time::sleep(Duration::from_secs(5)) => {209 eprintln!("timeout");210 }211 }212213 Ok(())214}215```216217### Channel Types218219| Channel | Use Case | Import |220|---------|----------|--------|221| `mpsc` | Multiple producers, single consumer | `tokio::sync::mpsc` |222| `oneshot` | Single value, single use | `tokio::sync::oneshot` |223| `broadcast` | Multiple consumers, all get every message | `tokio::sync::broadcast` |224| `watch` | Single value, latest-only (config reload) | `tokio::sync::watch` |225226**Deep dive**: Load `./references/async-tokio.md` for spawn patterns, graceful shutdown, Mutex choice, async traits, streams.227228## Cargo Quick Reference229230```bash231# Create project232cargo new my-project # binary233cargo new my-lib --lib # library234235# Build and run236cargo build # debug237cargo build --release # optimized238cargo run -- args # build + run239cargo run --example name # run example240241# Test242cargo test # all tests243cargo test test_name # specific test244cargo test -- --nocapture # show println output245246# Dependencies247cargo add serde --features derive # add dep248cargo add tokio -F full # shorthand249cargo update # update lock file250251# Check without building252cargo check # fast type checking253cargo clippy # lints254cargo fmt # format255256# Workspace257cargo test --workspace # test all crates258cargo build -p my-crate # build specific crate259```260261### Feature Flags262263```toml264[features]265default = ["json"]266json = ["dep:serde_json"]267full = ["json", "yaml", "toml"]268269[dependencies]270serde_json = { version = "1", optional = true }271```272273### Release Profile Tuning274275```toml276[profile.release]277lto = true # Link-time optimization: smaller, faster binaries278codegen-units = 1 # Better optimization at the cost of compile time279```280281## Common Gotchas282283| Gotcha | Why | Fix |284|--------|-----|-----|285| `String` vs `&str` | Owned vs borrowed, function signatures | Accept `&str` in params, return `String` |286| Borrow checker fight | Borrowing self while mutating | Split struct, use indices, clone (if cheap) |287| Lifetime elision confusion | Hidden lifetimes in function signatures | Write them out explicitly to understand, then elide |288| `impl Trait` in return | Different branches must return same type | Use `Box<dyn Trait>` for heterogeneous returns |289| `tokio::Mutex` vs `std::Mutex` | `std::Mutex` can't be held across `.await` | Use `tokio::Mutex` across await points |290| Orphan rule | Can't impl foreign trait for foreign type | Newtype pattern: `struct Wrapper(ForeignType)` |291| `Pin` confusion | Required for self-referential async futures | Use `Box::pin()`, don't fight it |292| `Send` bounds on async | Spawned futures must be `Send` | Avoid `Rc`, `RefCell` in async; use `Arc`, `Mutex` |293| `.unwrap()` in production | Panics on None/Err | Use `?`, `.unwrap_or()`, `.expect("reason")` |294295## serde Quick Reference296297```rust298use serde::{Serialize, Deserialize};299300#[derive(Serialize, Deserialize)]301#[serde(rename_all = "camelCase")]302struct User {303 user_id: i64,304 display_name: String,305306 #[serde(skip_serializing_if = "Option::is_none")]307 email: Option<String>,308309 #[serde(default)]310 is_active: bool,311312 #[serde(rename = "type")]313 user_type: UserType,314315 #[serde(with = "chrono::serde::ts_seconds")]316 created_at: DateTime<Utc>,317}318319// Serialize320let json = serde_json::to_string(&user)?;321let yaml = serde_yaml::to_string(&user)?;322323// Deserialize324let user: User = serde_json::from_str(&json)?;325```326327**Deep dive**: Load `./references/ecosystem.md` for serde advanced usage, clap, reqwest, sqlx, axum, tracing, rayon.328329## Reference Files330331Load these for deep-dive topics. Each is self-contained.332333| Reference | When to Load |334|-----------|-------------|335| `./references/ownership-lifetimes.md` | Borrowing rules, lifetime annotations, elision, interior mutability, common borrow checker patterns |336| `./references/traits-generics.md` | Trait design, associated types, supertraits, generics, constraints, sealed/extension traits |337| `./references/error-handling.md` | Result/Option combinators, thiserror/anyhow deep dive, error conversion, panic/recover |338| `./references/async-tokio.md` | tokio runtime, spawn, channels, select, streams, graceful shutdown, async traits, Mutex choice |339| `./references/ecosystem.md` | serde advanced, clap, reqwest, sqlx, axum, tracing, rayon, itertools, Cow |340| `./references/testing.md` | Unit/integration/doc tests, async tests, mockall, proptest, criterion benchmarks |341342## See Also343344- `docker-ops` - Multi-stage builds for Rust (scratch/distroless, cargo-chef for layer caching)345- `ci-cd-ops` - Rust CI pipelines, cargo caching, cross-compilation346- `testing-ops` - Cross-language testing strategies