๐Ÿ”งAutoSkills
All items
skillv1.0.0

rust-cargo

Rust + Cargo: ownership, error handling with thiserror/anyhow, iterators, async with tokio

rustcargolanguage

Install

$npx autoskills --items rust-cargo

Or scan + install everything matching your stack with npx autoskills.

Rust + Cargo

Rust rewards specificity. The compiler will tell you what's wrong; the question is whether your code shape lets it tell you the right thing.

Project layout

my-crate/
  Cargo.toml
  src/
    lib.rs          # library entry point (preferred for non-CLI projects)
    main.rs         # binary entry point
    bin/            # additional binaries (cargo run --bin x)
  tests/            # integration tests (one file = one test binary)
  benches/          # cargo bench
  examples/         # cargo run --example

For multi-crate projects, use a workspace:

# Cargo.toml at root
[workspace]
members = ["crates/*"]
resolver = "2"

Workspaces share target/, lockfile, and dependency resolution.

Error handling

Two patterns; pick by layer:

Library code โ†’ thiserror:

use thiserror::Error;
 
#[derive(Error, Debug)]
pub enum DbError {
    #[error("connection failed: {0}")]
    Connection(#[from] std::io::Error),
    #[error("query failed: {0}")]
    Query(String),
}

Application code โ†’ anyhow:

use anyhow::{Context, Result};
 
fn read_config(path: &str) -> Result<Config> {
    let raw = std::fs::read_to_string(path)
        .with_context(|| format!("reading {}", path))?;
    toml::from_str(&raw).context("parsing config")
}
  • ? propagates errors โ€” chain operations without match ladders.
  • Add .context() at every layer boundary; the resulting error reads like a stack trace.
  • Result<T> aliases at module level: pub type Result<T> = std::result::Result<T, MyError>;

Ownership patterns

  • Take &str over String for inputs you don't keep. Callers can pass either.
  • Return String when you build one. Don't borrow from local data.
  • Cow<'_, str> when you sometimes-allocate-sometimes-borrow.
  • Arc<T> for shared immutable state across threads, Arc<Mutex<T>> for shared mutable.
  • Avoid Rc<RefCell<T>> in async / multithreaded code โ€” not Send.

Common allocations to avoid

// bad: allocates a String just to compare
if name.to_string() == "alice" { ... }
 
// good
if name == "alice" { ... }
 
// bad: collect just to count
let count = items.iter().filter(|x| x.active).collect::<Vec<_>>().len();
 
// good
let count = items.iter().filter(|x| x.active).count();
 
// bad: clone in a loop
for s in &strings {
    process(s.clone());
}
 
// good: borrow if process takes &str / &String
for s in &strings {
    process(s);
}

Iterators over loops

// idiomatic
let evens: Vec<i32> = (0..100).filter(|n| n % 2 == 0).collect();
 
// less idiomatic
let mut evens = Vec::new();
for n in 0..100 {
    if n % 2 == 0 { evens.push(n); }
}

Iterator chains compile to tight loops โ€” no performance cost.

Pattern matching

// exhaustive with discriminated enums
match status {
    Status::Active => process(),
    Status::Banned(reason) => log_ban(reason),
    Status::Pending { since } => wait(since),
}
 
// guard clauses with `if`
match age {
    n if n < 18 => "minor",
    18..=64    => "adult",
    _          => "senior",
}
 
// `if let` for single-arm extraction
if let Some(user) = find_user(id) { ... }
 
// `let else` for early-return narrowing (Rust 1.65+)
let Some(user) = find_user(id) else { return Err("not found"); };

Async (tokio)

  • #[tokio::main] for the binary entry, #[tokio::test] for tests.
  • tokio::spawn for fire-and-forget; tokio::join! for waiting on multiple.
  • Mutex<T> from tokio, not std, in async code.
  • Don't hold a MutexGuard across an .await โ€” deadlocks waiting.
  • tokio::sync::mpsc for channels; watch for "latest value" semantics.

Testing

#[cfg(test)]
mod tests {
    use super::*;
 
    #[test]
    fn it_works() {
        assert_eq!(add(2, 2), 4);
    }
 
    #[tokio::test]
    async fn async_works() {
        assert_eq!(fetch().await, "ok");
    }
}
  • Unit tests in the same file as the code (#[cfg(test)] mod tests).
  • Integration tests in tests/ โ€” see public API only, one file per topic.
  • #[should_panic(expected = "...")] for panic assertions.
  • assert_matches! (from assert_matches crate) for pattern matching in assertions.

Cargo conventions

  • Cargo.toml features over branches in code for optional functionality.
  • Pin dependencies in Cargo.toml to caret ranges (^1.2.3 = >=1.2.3, <2.0.0). Don't pin to exact unless you have a reason.
  • cargo fmt + cargo clippy -- -D warnings in CI. Non-negotiable.
  • cargo nextest as the test runner โ€” parallel, faster than cargo test, better output.

What to avoid

  • unwrap() / expect() in library code โ€” panics there are bugs you push onto callers.
  • String::from(s) when s.to_string() reads better โ€” they compile identically; pick by readability.
  • Premature trait abstraction โ€” start concrete; add traits when you have two implementations.
  • Box<dyn Trait> reflexively โ€” generics are usually better. Use dyn for heterogeneous collections.
  • Silencing clippy lints with #[allow] without a // reason: ... comment.