# Rust

> Ownership and borrowing in practice, error handling, async, and the cargo commands that matter day to day.

Canonical: https://www.wiki.jodisand.me/rust/
Reviewed: 2026-09-24
Related: [Go](https://www.wiki.jodisand.me/go/index.md), [Testing](https://www.wiki.jodisand.me/testing/index.md), [Python](https://www.wiki.jodisand.me/python/index.md)


Rust gives you memory safety and data-race freedom without a garbage collector by checking ownership and borrowing at compile time. The cost is that the compiler rejects programs a runtime-checked language would accept, so day-to-day work is largely about satisfying the borrow checker and choosing the right ownership shape. The sections below cover the rules that trip people up and the `cargo` commands that keep the loop fast. Check APIs against [doc.rust-lang.org](https://doc.rust-lang.org/std/) and tooling against the [Cargo book](https://doc.rust-lang.org/cargo/).

## Cheatsheet

| Task | Command |
| --- | --- |
| Check without codegen (fast) | `cargo check --all-targets` |
| Build optimised | `cargo build --release` |
| Run tests, show output | `cargo test -- --nocapture` |
| One test | `cargo test path::to::test_name` |
| Lint hard | `cargo clippy --all-targets -- -D warnings` |
| Format | `cargo fmt --all` (check: `--check`) |
| Explain an error code | `rustc --explain E0502` |
| Expand a macro | `cargo expand` |
| Dependency tree | `cargo tree -d` (duplicates) |
| Audit advisories | `cargo audit` |
| Update within semver | `cargo update` |
| Add a dependency | `cargo add serde --features derive` |
| Benchmark | `cargo bench` (or criterion) |
| Docs for this crate and deps | `cargo doc --open` |
| Binary size breakdown | `cargo bloat --release` |
| MSRV / toolchain pin | `rust-toolchain.toml` |

## Ownership in practice

Every value has exactly one owner, and the value is dropped when that owner leaves scope. You may hold many shared references (`&T`) at once, or exactly one mutable reference (`&mut T`), but never both to the same data at the same time. This is what makes a data race and a use-after-free impossible to compile, and it is the rule behind most borrow-checker errors.

```rust
let s = String::from("hello");
let t = s;                  // move: ownership transfers, s is no longer usable
let u = t.clone();          // an explicit deep copy when you genuinely need two owners

fn read(v: &[u8]) {}        // borrow: the caller keeps ownership
fn consume(v: Vec<u8>) {}   // take ownership: the caller cannot use the value afterwards
```

Prefer borrowing in function signatures: take `&str` and `&[T]`, return `String` and `Vec<T>`. That gives callers the most freedom and does not force an allocation just to call you.

| Error | What it means | Usual fix |
| --- | --- | --- |
| `E0382` use of moved value | You gave the value away and used it again | Borrow instead, or `clone` deliberately |
| `E0502` mutable borrow while borrowed | A shared and a mutable borrow overlap | Shorten the first borrow's scope, or restructure |
| `E0499` two mutable borrows | Aliased mutation of the same data | Split the data (`split_at_mut`), or index instead |
| `E0597` does not live long enough | A reference outlives the value it points to | Own the data, or tie the lifetimes explicitly |
| `E0308` mismatched types | Often `String` vs `&str`, or `T` vs `&T` | `&x`, `x.as_str()`, or `x.to_owned()` |

When a message is opaque, `rustc --explain E05xx` prints a worked example of the same error.

## Errors

`Result<T, E>` is how a fallible function reports failure, and `?` propagates it up, converting the error type through a `From` impl on the way. Libraries expose a concrete, matchable error type so callers can branch; applications usually flatten everything into one contextual type. This is Rust's answer to what [Go](https://www.wiki.jodisand.me/go/#errors) does with wrapped error values.

```rust
use thiserror::Error;

#[derive(Debug, Error)]
pub enum StoreError {
    #[error("user {0} not found")]
    NotFound(i64),
    #[error("database: {0}")]
    Db(#[from] sqlx::Error),      // generates From<sqlx::Error>, so ? converts automatically
}

pub fn get(id: i64) -> Result<User, StoreError> {
    let row = query(id)?;          // sqlx::Error becomes StoreError::Db via #[from]
    row.ok_or(StoreError::NotFound(id))
}
```

```rust
use anyhow::{Context, Result};

fn main() -> Result<()> {
    let cfg = std::fs::read_to_string("config.toml")
        .context("reading config.toml")?;     // attaches a message, keeps the underlying source
    Ok(())
}
```

Use `thiserror` for libraries (typed, matchable) and `anyhow` for binaries (contextual, printable). `unwrap()` in production is a claim that the failure case is impossible; `expect("reason")` makes at least the reason visible in the panic message when the claim turns out to be wrong.

## Options and iterators

`Option<T>` encodes "value or nothing" in the type, so the compiler forces you to handle absence instead of dereferencing a null. Iterators are lazy adapters that compile down to the same machine code as a hand-written loop, so chaining `map`, `filter` and the rest costs nothing at runtime; nothing runs until a consumer like `collect`, `sum` or `for` drives it.

```rust
let name: Option<&str> = map.get("name").map(String::as_str);

let total: u64 = items.iter().filter(|i| i.active).map(|i| i.bytes).sum();
let first_err = results.iter().find_map(|r| r.as_ref().err());
let parsed: Result<Vec<i32>, _> = inputs.iter().map(|s| s.parse::<i32>()).collect();  // stops at the first Err
let (ok, bad): (Vec<_>, Vec<_>) = results.into_iter().partition(Result::is_ok);

value.unwrap_or_default();          // the type's default when None
value.unwrap_or_else(|| expensive());   // computed only when None
value.ok_or(Error::Missing)?;       // Option to Result, then propagate
```

Collecting into `Result<Vec<_>, E>` is the idiom for "all or nothing": it returns the first error or the full vector.

## Structs, traits and generics

Traits define shared behaviour; generics let one function work over any type that implements a trait. Static dispatch (`<S: Store>`) monomorphises a copy per concrete type: fastest, largest binary. Dynamic dispatch (`&dyn Store`) keeps one copy behind a vtable: smaller code, one pointer indirection, and it lets you choose the type at runtime.

```rust
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct Config {
    pub name: String,
    #[serde(default = "default_port")]      // called when the field is absent from the input
    pub port: u16,
}

pub trait Store {
    fn get(&self, id: i64) -> Result<User, StoreError>;
}

fn handler<S: Store>(store: &S) {}          // static dispatch, monomorphised per type
fn handler_dyn(store: &dyn Store) {}        // dynamic dispatch, one copy of the code
```

Reach for generics by default and `dyn Trait` when the concrete type must be chosen at runtime or code size matters. `impl Trait` in argument position is shorthand for the generic form.

## Shared state and concurrency

The right wrapper depends on two questions: is the value shared across threads, and does it need mutation. Get this wrong and the compiler tells you, because `Send` and `Sync` are checked; the table below is the usual mapping.

| Need | Type |
| --- | --- |
| Single owner, single thread | Plain value |
| Shared, single thread | `Rc<T>` / `Rc<RefCell<T>>` |
| Shared across threads, read-only | `Arc<T>` |
| Shared and mutable across threads | `Arc<Mutex<T>>` or `Arc<RwLock<T>>` |
| Counter | `AtomicU64` |
| Message passing | `std::sync::mpsc` or `crossbeam` / `tokio::sync::mpsc` |

```rust
let state = Arc::new(Mutex::new(HashMap::new()));
let s = Arc::clone(&state);                // bump the refcount, not the data
std::thread::spawn(move || {
    s.lock().unwrap().insert("k", 1);      // lock() errors only if a previous holder panicked (poisoning)
});
```

A `std::sync::MutexGuard` held across an `.await` deadlocks an async runtime, because the task can be parked while still holding the lock. Use `tokio::sync::Mutex` there, or drop the guard before awaiting.

## Async

`async` functions return futures that do nothing until polled by a runtime such as Tokio. Concurrency comes from driving many futures on a few threads, so anything that blocks a thread (file I/O, `std::thread::sleep`, CPU-bound work) stalls every task on that worker. Move blocking work to `tokio::task::spawn_blocking`.

```rust
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let client = reqwest::Client::builder().timeout(Duration::from_secs(10)).build()?;

    let (a, b) = tokio::try_join!(fetch(&client, "/a"), fetch(&client, "/b"))?;   // both, fail on first error

    let results = futures::future::join_all(ids.iter().map(|id| fetch_one(&client, *id))).await;

    tokio::select! {                          // whichever branch completes first wins
        res = work() => res?,
        _ = tokio::time::sleep(Duration::from_secs(5)) => anyhow::bail!("timeout"),
    }
    Ok(())
}
```

## Tests

Unit tests live beside the code in a `#[cfg(test)] mod tests` and can reach private items; integration tests live in `tests/` and see only the public API, which makes them the honest check of your interface. See [testing](https://www.wiki.jodisand.me/testing/) for property-based and snapshot approaches.

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_valid_input() {
        assert_eq!(parse("a=1").unwrap(), Config { name: "a".into(), port: 1 });
    }

    #[test]
    fn rejects_empty() {
        assert!(matches!(parse(""), Err(ParseError::Empty)));   // match the variant, ignore its fields
    }

    #[tokio::test]
    async fn fetches() { /* an async test on the Tokio runtime */ }
}
```

## Cargo and builds

`cargo` drives the whole workflow: `check` type-checks without codegen for the fastest feedback, `clippy` adds lints beyond the compiler's, and the release profile controls the size and speed of the shipped binary.

```toml
[profile.release]
lto = "thin"           # link-time optimisation across crates; "fat" is slower to build, marginally smaller
codegen-units = 1      # one unit gives the optimiser the whole crate, at the cost of parallel build time
panic = "abort"        # smaller and faster, but no stack unwinding and no catch_unwind
strip = "symbols"      # drop symbol tables from the binary
```

```sh
cargo build --release --target x86_64-unknown-linux-musl   # fully static binary for a scratch image
cargo test --workspace --all-features
cargo clippy --all-targets --all-features -- -D warnings   # treat every lint as an error, for CI
cargo tree -i openssl                                      # invert the tree: what pulls openssl in
cargo update -p serde --precise 1.0.203                    # pin one dependency to an exact version
```

Commit `Cargo.lock` for binaries so builds are reproducible; omit it for libraries so downstream crates resolve their own versions. Pin the toolchain with `rust-toolchain.toml` so CI and laptops compile with the same compiler.

## Lifetimes and smart pointers

A lifetime annotation does not change how long anything lives; it tells the compiler how the lifetimes of inputs and outputs relate so it can check that no reference outlives its data. Most signatures need none because of elision: one input reference means the output borrows from it. You write them when a function returns a reference and takes more than one, or when a struct holds a reference.

```rust
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b } }

struct Parser<'src> { input: &'src str, pos: usize }   // the struct cannot outlive the text it points into

fn first_word(s: &str) -> &str { s.split(' ').next().unwrap_or("") }   // elided: output borrows from s

const NAME: &'static str = "api";                      // lives for the whole program
```

A struct that borrows is cheap but infectious: everything holding it needs the lifetime too. When that spreads into async code or long-lived state, own the data (`String`, `Vec<T>`) or share it with `Arc<str>` instead. `Box<T>` puts a value on the heap with one owner, which is how recursive types (`enum Expr { Add(Box<Expr>, Box<Expr>) }`) and trait objects (`Box<dyn Error>`) get a known size. `Cow<'a, str>` holds either a borrow or an owned string, so a function that usually returns its input unchanged allocates only on the rare path that modifies it.

Interior mutability (`Cell`, `RefCell`, `Mutex`) moves the aliasing check from compile time to runtime. `RefCell::borrow_mut` panics if a borrow is already live; that panic is the same bug the borrow checker would have caught, surfacing later. Use it for caches and graph-like structures where the static rules are too coarse, not as a habit.

## Iterator patterns

An iterator adapter chain is a description of a computation; `collect`, `for`, `sum`, `count` and friends run it. Because adapters are lazy, `iter().map(f).take(3)` calls `f` three times, not once per element. The type annotation on `collect` chooses the container.

```rust
let names: Vec<&str> = users.iter().map(|u| u.name.as_str()).collect();
let by_id: HashMap<i64, &User> = users.iter().map(|u| (u.id, u)).collect();
let joined = names.join(",");                                           // Vec<&str> and Vec<String> both work

for (i, line) in text.lines().enumerate().skip(1) {}                    // header skipped, 1-based i
let pairs: Vec<(u8, char)> = bytes.iter().copied().zip(chars).collect();
let windows = readings.windows(2).filter(|w| w[1] > w[0]).count();      // consecutive overlapping pairs
let chunks: Vec<Vec<Job>> = jobs.chunks(100).map(<[Job]>::to_vec).collect();
let flat: Vec<Item> = pages.into_iter().flat_map(|p| p.items).collect();
let total = costs.iter().fold(0u64, |acc, c| acc.saturating_add(*c));   // no overflow panic
let max_by_len = words.iter().max_by_key(|w| w.len());
let grouped = items.iter().fold(HashMap::<_, Vec<_>>::new(), |mut m, i| { m.entry(i.kind).or_default().push(i); m });
let sorted = { let mut v = items.clone(); v.sort_by(|a, b| b.score.total_cmp(&a.score)); v };   // floats need total_cmp
```

`iter()` borrows, `iter_mut()` borrows mutably, `into_iter()` consumes. A `for x in &v` loop is `v.iter()`. When a closure needs to fail, collect into `Result<Vec<_>, E>` or use `try_for_each`; when it needs an index, `enumerate` rather than `0..v.len()`. `itertools` adds `chunk_by`, `sorted_by_key`, `unique` and `join` on any iterator, worth the dependency in data-heavy code.

## Trait design

A trait's methods can have default bodies, and a trait can declare associated types and constants that each implementation fixes. `impl Trait for Type` can live in either the trait's crate or the type's crate (the orphan rule), so wrapping a foreign type in a newtype (`struct Bytes(Vec<u8>)`) is how you implement a foreign trait for it. Derive standard traits liberally: `Debug` on everything, `Clone` when copying is reasonable, `PartialEq`/`Eq`/`Hash` for map keys, `Default` for config-like structs.

```rust
pub trait Backend {
    type Conn: Send;                                          // associated type, chosen by the implementor
    const NAME: &'static str;
    fn connect(&self) -> Result<Self::Conn, StoreError>;
    fn describe(&self) -> String { format!("backend {}", Self::NAME) }   // default method
}

impl std::fmt::Display for Host {                             // Display gives you to_string() for free
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}:{}", self.name, self.port) }
}

impl From<Row> for Host {                                     // From gives Into; ? uses From for errors
    fn from(r: Row) -> Self { Host { name: r.name, port: r.port } }
}

impl std::str::FromStr for Level {                            // "info".parse::<Level>()
    type Err = ParseError;
    fn from_str(s: &str) -> Result<Self, Self::Err> { match s { "info" => Ok(Level::Info), _ => Err(ParseError::Unknown(s.into())) } }
}

fn run(backends: &[Box<dyn Backend<Conn = PgConn>>]) {}       // trait objects need the associated type fixed
```

A trait is object-safe (usable as `dyn Trait`) only when none of its methods are generic or return `Self` by value. `where` clauses read better than inline bounds once there are two or more: `fn send<T>(msg: T) where T: Serialize + Send + 'static`. `AsRef<str>` and `AsRef<Path>` in argument position accept `&str`, `String`, `&String` and more without the caller converting. Since Rust 1.75 traits may have `async fn` directly; for a `dyn`-compatible async trait, the `async-trait` crate boxes the returned future.

## Workspaces and features

A workspace shares one `Cargo.lock` and `target/` across several crates, so a service and its library split compile once and stay version-aligned. Feature flags compile optional code and dependencies; they are additive, meaning a crate must work with any combination enabled, and every dependency in the graph that enables a feature turns it on for everyone.

```toml
# Cargo.toml at the workspace root
[workspace]
members = ["crates/*"]
resolver = "3"                       # edition 2024 resolver; "2" for edition 2021

[workspace.package]
edition = "2024"
license = "MIT"

[workspace.dependencies]             # one version, inherited by members with `serde.workspace = true`
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }

[workspace.lints.clippy]
unwrap_used = "warn"
```

```toml
# crates/api/Cargo.toml
[features]
default = ["json"]
json = ["dep:serde_json"]            # dep: prefix: the feature enables an optional dependency without exposing it
metrics = []                         # gates #[cfg(feature = "metrics")] code only

[dependencies]
serde_json = { version = "1", optional = true }
```

```sh
cargo build -p api                                 # one workspace member
cargo test --workspace --no-default-features       # prove the crate compiles with nothing optional
cargo build --features metrics --no-default-features
cargo hack check --each-feature --no-dev-deps      # cargo-hack: every feature alone, catches missing cfg guards
cargo tree -e features -p api                      # which features are active and who enabled them
cargo metadata --format-version 1 | jq '.workspace_members'
```

Edition 2024 (Rust 1.85+) changes `unsafe extern`, `gen` as a reserved keyword and the default lifetime capture rules in `impl Trait` return types; `cargo fix --edition` migrates a crate mechanically, and each crate in a workspace migrates independently.

## Clippy

Clippy groups lints by intent: `correctness` (deny by default), `suspicious`, `style`, `complexity`, `perf` (all warn), `pedantic`, `nursery` and `restriction` (allow, opt in). Configure at the workspace root with `[workspace.lints]` (Rust 1.74+) rather than per-file attributes, so every crate agrees, and treat warnings as errors in CI only.

```toml
[workspace.lints.clippy]
all = { level = "warn", priority = -1 }    # lower priority so the specific lints below override the group
pedantic = { level = "warn", priority = -1 }
module_name_repetitions = "allow"
missing_errors_doc = "allow"
unwrap_used = "warn"                        # restriction lint: forces expect() or ? in library code
dbg_macro = "warn"
todo = "warn"

[workspace.lints.rust]
unsafe_code = "forbid"
missing_debug_implementations = "warn"
```

```sh
cargo clippy --all-targets --all-features -- -D warnings   # CI: any warning fails
cargo clippy --fix --allow-dirty                            # apply machine-applicable suggestions in a dirty tree
cargo clippy -- -W clippy::pedantic 2>&1 | grep -c warning  # count before committing to a group
```

`#[allow(clippy::too_many_arguments)]` on one function with a comment is fine; `#![allow(clippy::all)]` at the crate root means the lint budget has been spent. `clippy.toml` beside `Cargo.toml` holds thresholds such as `too-many-arguments-threshold = 8` and `msrv = "1.80"`, which stops Clippy suggesting APIs newer than your minimum supported toolchain.

## Tokio basics

Tokio is a multi-threaded work-stealing runtime by default: `#[tokio::main]` starts one worker per core, and a task moves between workers at any `.await`, which is why a spawned future must be `Send + 'static`. `tokio::spawn` returns a `JoinHandle` that must be awaited or the task's error is silently dropped; a `JoinSet` tracks many. Blocking the thread (`std::fs`, `std::net`, a heavy computation, a `std::sync::Mutex` under contention) stalls every task scheduled on that worker.

```rust
use tokio::sync::{mpsc, oneshot, watch, Semaphore};
use tokio::task::JoinSet;

let (tx, mut rx) = mpsc::channel::<Job>(256);              // bounded: send().await backpressures the producer
let (done_tx, done_rx) = oneshot::channel::<Result<(), Error>>();   // one reply
let (cfg_tx, cfg_rx) = watch::channel(Config::default());  // latest value, many readers: config reload

let mut set = JoinSet::new();
for id in ids {
    let client = client.clone();
    set.spawn(async move { fetch(&client, id).await });
}
while let Some(res) = set.join_next().await {
    match res {
        Ok(Ok(v)) => handle(v),
        Ok(Err(e)) => tracing::warn!(error = %e, "fetch failed"),   // the task's own error
        Err(join) if join.is_panic() => tracing::error!("task panicked"),
        Err(_) => {}                                                  // cancelled
    }
}

let sem = Arc::new(Semaphore::new(16));                    // bound concurrency across spawned tasks
let permit = sem.clone().acquire_owned().await?;           // held by the task; released on drop
tokio::spawn(async move { let _permit = permit; work().await });

let result = tokio::time::timeout(Duration::from_secs(5), call()).await;   // Err(Elapsed) on timeout
let handle = tokio::task::spawn_blocking(move || compress(&data));         // runs on the blocking pool
```

Cancellation in Tokio is dropping the future: `select!` drops the losing branches, `timeout` drops the inner future, and a dropped `JoinHandle` detaches rather than cancels (call `abort()` to cancel). A future that holds a resource and is dropped mid-await must be cancel-safe, which `mpsc::Receiver::recv` is and a half-read `read_exact` is not; the Tokio docs mark each method. `tokio::signal::ctrl_c()` and `tokio::signal::unix::signal(SignalKind::terminate())` are the hooks for graceful shutdown, paired with `tokio_util::sync::CancellationToken` to tell every task to stop. `tokio-console` attaches to a runtime built with the `tracing` instrumentation and shows which tasks are busy, idle or never polled.

## Troubleshooting

| Symptom | Cause | Check |
| --- | --- | --- |
| Borrow-checker error you cannot see | The message names lines, not the real overlap | `rustc --explain E05xx`; shorten the borrow's scope or clone |
| `cannot move out of ... borrowed content` | Taking ownership through a reference | Borrow the field, `clone`, or restructure to own it |
| Rebuild recompiles the world | A `build.rs`, a proc-macro dep or a changed feature flag | `cargo build --timings`; check `cargo tree -e features` |
| Two versions of the same crate | Semver-incompatible requirements pull both | `cargo tree -d`; align versions or `cargo update -p crate --precise` |
| Async task never completes | A future not awaited, or a `std` mutex guard held across `.await` | Confirm the `.await`; use `tokio::sync::Mutex` and drop guards early |
| Deadlock under load | Inconsistent lock ordering, or a blocking `std` mutex in async code | Acquire locks in a fixed order; move blocking work to `spawn_blocking` |
| `SIGABRT` with no unwind | `panic = "abort"` in the release profile | Expected; `catch_unwind` needs `panic = "unwind"` |
| Links against system OpenSSL unexpectedly | A transitive dependency on `openssl` | `cargo tree -i openssl`; switch to `rustls` or enable its `vendored` feature |
| `cargo audit` reports an advisory | A vulnerable version in `Cargo.lock` | `cargo update -p crate`, then re-run `cargo audit` |
| `future cannot be sent between threads safely` | A non-`Send` value (`Rc`, `RefCell` guard, `std::sync::MutexGuard`) lives across an `.await` | Scope the value so it drops before the `.await`; use `Arc` or Tokio's `Mutex` |
| `already borrowed: BorrowMutError` panic | Two live `RefCell` borrows | Shorten the first borrow; consider whether ownership can be restructured |
| `the trait ... cannot be made into an object` | Generic method or `Self` return in the trait | Move the generic method to an extension trait, or add `where Self: Sized` |
| Feature-gated code compiles alone but fails in the workspace | Feature unification enabled a combination you never tested | `cargo hack check --each-feature`; guard code with the right `cfg` |
| Task output never appears | `JoinHandle` dropped or never awaited | Await or `abort()` the handle; use `JoinSet` |
| Runtime hangs on shutdown | A spawned task loops without a cancellation check | Pass a `CancellationToken`; `tokio::time::timeout` around `join` |
| `cargo clippy` clean locally, fails in CI | Different toolchain version | Pin `rust-toolchain.toml`; match `msrv` in `clippy.toml` |
| Slow incremental compile after a small edit | Change in a crate everything depends on | Split the leaf crate out; check `cargo build --timings` |

## Oneliners

```sh
# Explain the error you just got
rustc --explain E0502

# Fastest feedback loop: recheck and retest on save
cargo watch -x check -x test

# Show where time goes in a slow build
cargo build --release --timings && open target/cargo-timings/cargo-timing.html

# Duplicate dependency versions bloating the build
cargo tree -d

# Unused dependencies (nightly tool)
cargo +nightly udeps

# Which features are enabled for a crate and why
cargo tree -e features -i tokio

# Expand a derive or macro to see the generated code
cargo expand --lib path::to::module

# Static musl binary, then confirm it has no dynamic links
cargo build --release --target x86_64-unknown-linux-musl && ldd target/x86_64-unknown-linux-musl/release/app

# Type-check the whole workspace without producing artefacts
cargo check --workspace --all-targets --all-features

# Fail CI on formatting
cargo fmt --all -- --check

# Security advisories against the lock file
cargo audit --deny warnings

# Run Miri to catch undefined behaviour in unsafe code
cargo +nightly miri test

# Print the exact toolchain in use, for a bug report or CI log
rustc -vV && cargo --version

# Run only tests whose name contains a substring, single-threaded, with logs
RUST_LOG=debug cargo test parse -- --test-threads=1 --nocapture

# Check the crate builds with the minimum supported Rust version declared in Cargo.toml
cargo +1.80 check --locked

# Symbolised backtrace on panic
RUST_BACKTRACE=1 cargo run

# Everything that would be published, to catch stray files
cargo package --list

# Build docs with private items and fail on broken intra-doc links
RUSTDOCFLAGS='-D warnings' cargo doc --no-deps --document-private-items
```

## Snippets

Retry with exponential backoff on a transient error class, cancellable by a token.

```rust
async fn retry<T, E, F, Fut>(attempts: u32, token: &CancellationToken, mut f: F) -> Result<T, E>
where
    F: FnMut() -> Fut,
    Fut: Future<Output = Result<T, E>>,
    E: std::fmt::Display,
{
    let mut delay = Duration::from_millis(200);
    for attempt in 1..=attempts {
        match f().await {
            Ok(v) => return Ok(v),
            Err(e) if attempt == attempts => return Err(e),
            Err(e) => tracing::warn!(attempt, error = %e, "retrying in {delay:?}"),
        }
        tokio::select! {
            _ = tokio::time::sleep(delay) => {}
            _ = token.cancelled() => break,
        }
        delay = (delay * 2).min(Duration::from_secs(10));
    }
    f().await                                                        // final attempt after a cancellation
}
```

Worker pool over a bounded channel: producers back-pressure, workers stop when the sender drops.

```rust
let (tx, rx) = tokio::sync::mpsc::channel::<Job>(64);
let rx = Arc::new(tokio::sync::Mutex::new(rx));
let mut workers = JoinSet::new();
for _ in 0..8 {
    let rx = Arc::clone(&rx);
    workers.spawn(async move {
        loop {
            let job = { rx.lock().await.recv().await };              // guard dropped before processing
            match job { Some(j) => process(j).await, None => break }  // None: every sender is gone
        }
    });
}
for j in jobs { tx.send(j).await?; }
drop(tx);                                                            // signals completion
while workers.join_next().await.is_some() {}
```

Per-call timeout with a typed error, so the caller can tell a timeout from a failure.

```rust
#[derive(Debug, thiserror::Error)]
enum CallError {
    #[error("timed out after {0:?}")] Timeout(Duration),
    #[error(transparent)] Upstream(#[from] reqwest::Error),
}

async fn call(client: &reqwest::Client, url: &str, budget: Duration) -> Result<String, CallError> {
    tokio::time::timeout(budget, async { client.get(url).send().await?.error_for_status()?.text().await })
        .await
        .map_err(|_| CallError::Timeout(budget))?
}
```

Configuration from a TOML file overlaid with environment variables, via `serde` and `figment`-free code.

```rust
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
    pub listen: String,
    pub database_url: String,
    #[serde(with = "humantime_serde")] pub timeout: Duration,      // "5s" in the file
    pub workers: usize,
}
impl Default for Config {
    fn default() -> Self { Self { listen: "127.0.0.1:8080".into(), database_url: String::new(), timeout: Duration::from_secs(5), workers: 4 } }
}

pub fn load(path: &Path) -> anyhow::Result<Config> {
    let mut cfg: Config = match std::fs::read_to_string(path) {
        Ok(s) => toml::from_str(&s).with_context(|| format!("parsing {}", path.display()))?,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Config::default(),
        Err(e) => return Err(e).context("reading config"),
    };
    if let Ok(v) = std::env::var("APP_DATABASE_URL") { cfg.database_url = v; }
    anyhow::ensure!(!cfg.database_url.is_empty(), "database_url is required");
    Ok(cfg)
}
```

HTTP client with connect and total timeouts, connection pooling and a user agent, built once and cloned.

```rust
let client = reqwest::Client::builder()
    .connect_timeout(Duration::from_secs(3))
    .timeout(Duration::from_secs(30))                    // whole request, including the body
    .pool_idle_timeout(Duration::from_secs(90))
    .pool_max_idle_per_host(32)
    .user_agent(concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")))
    .build()?;                                            // Client is an Arc internally; clone freely
```

Graceful shutdown on SIGTERM or Ctrl-C with a cancellation token and a drain deadline.

```rust
let token = CancellationToken::new();
let shutdown = {
    let token = token.clone();
    async move {
        let mut term = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?;
        tokio::select! { _ = tokio::signal::ctrl_c() => {}, _ = term.recv() => {} }
        tracing::info!("shutdown signal received");
        token.cancel();
        anyhow::Ok(())
    }
};
tokio::spawn(shutdown);

let server = axum::serve(listener, app).with_graceful_shutdown(token.clone().cancelled_owned());
tokio::time::timeout(Duration::from_secs(20), server).await??;   // give in-flight requests 20s
```

Structured logging with `tracing`: JSON to stderr, level from `RUST_LOG`, spans carrying request context.

```rust
use tracing_subscriber::{fmt, EnvFilter, prelude::*};

tracing_subscriber::registry()
    .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,hyper=warn")))
    .with(fmt::layer().json().with_current_span(true).with_writer(std::io::stderr))
    .init();

#[tracing::instrument(skip(db), fields(user_id = %id), err)]   // err: logs the Err at ERROR level automatically
async fn load_user(db: &Pool, id: i64) -> Result<User, StoreError> {
    tracing::debug!("querying");
    db.get(id).await
}
```

Newtype with validation at construction, so invalid values cannot exist downstream.

```rust
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
#[serde(transparent)]
pub struct Port(u16);

impl TryFrom<u16> for Port {
    type Error = ConfigError;
    fn try_from(v: u16) -> Result<Self, Self::Error> { if v == 0 { Err(ConfigError::ZeroPort) } else { Ok(Port(v)) } }
}
impl<'de> serde::Deserialize<'de> for Port {
    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
        Port::try_from(u16::deserialize(d)?).map_err(serde::de::Error::custom)
    }
}
```

Read a large file line by line without loading it.

```rust
let file = std::fs::File::open(path)?;
for line in std::io::BufReader::new(file).lines() {
    let line = line?;                                     // io::Error on a bad read, not a panic
    if line.starts_with('#') { continue; }
    handle(&line);
}
```

Atomic write: temp file in the same directory, sync, rename.

```rust
fn write_atomic(path: &Path, data: &[u8]) -> std::io::Result<()> {
    let dir = path.parent().unwrap_or(Path::new("."));
    let mut tmp = tempfile::NamedTempFile::new_in(dir)?;
    tmp.write_all(data)?;
    tmp.as_file().sync_all()?;
    tmp.persist(path).map(|_| ()).map_err(|e| e.error)   // rename; the temp file is removed on any earlier error
}
```

Run a subprocess with a timeout and capture output.

```rust
let out = tokio::time::timeout(
    Duration::from_secs(60),
    tokio::process::Command::new("pg_dump").arg("--format=custom").arg(&db_url).kill_on_drop(true).output(),
).await.context("pg_dump timed out")??;
anyhow::ensure!(out.status.success(), "pg_dump failed: {}", String::from_utf8_lossy(&out.stderr).trim());
```

Table-driven test with a data slice and a message that names the failing case.

```rust
#[test]
fn parses_durations() {
    let cases: &[(&str, Option<Duration>)] = &[
        ("5s", Some(Duration::from_secs(5))),
        ("2m", Some(Duration::from_secs(120))),
        ("", None),
        ("bad", None),
    ];
    for (input, want) in cases {
        assert_eq!(parse_duration(input).ok(), *want, "input {input:?}");
    }
}
```

Shared read-mostly state with `RwLock` and a reload path that swaps the whole value.

```rust
let rules: Arc<RwLock<Arc<Rules>>> = Arc::new(RwLock::new(Arc::new(load_rules()?)));

// hot path: clone the inner Arc, drop the read lock immediately
let current = Arc::clone(&*rules.read().unwrap());
current.matches(&event);

// reload: build the new value outside the lock, then swap
let fresh = Arc::new(load_rules()?);
*rules.write().unwrap() = fresh;
```


