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.
Source
View on GitHubRust + 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 --exampleFor 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 withoutmatchladders.- 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
&stroverStringfor inputs you don't keep. Callers can pass either. - Return
Stringwhen 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 โ notSend.
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::spawnfor fire-and-forget;tokio::join!for waiting on multiple.Mutex<T>from tokio, not std, in async code.- Don't hold a
MutexGuardacross an.awaitโ deadlocks waiting. tokio::sync::mpscfor channels;watchfor "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!(fromassert_matchescrate) for pattern matching in assertions.
Cargo conventions
Cargo.tomlfeatures over branches in code for optional functionality.- Pin dependencies in
Cargo.tomlto 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 warningsin CI. Non-negotiable.cargo nextestas the test runner โ parallel, faster thancargo test, better output.
What to avoid
unwrap()/expect()in library code โ panics there are bugs you push onto callers.String::from(s)whens.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. Usedynfor heterogeneous collections.- Silencing
clippylints with#[allow]without a// reason: ...comment.