name: rust description: >- Rust language conventions, idioms, error handling, concurrency (CPU-bound parallelism with threads/rayon and I/O-bound async with Tokio), the cargo/clippy/rustfmt toolchain, and rustdoc. Invoke whenever task involves any interaction with Rust code — writing, reviewing, refactoring, debugging, or understanding .rs files and Cargo projects.
Rust
Lean on the type system and the borrow checker. They are the design surface, not an obstacle. When the compiler rejects
code, the ownership model is wrong — restructure who owns and who borrows rather than reaching for .clone(), Rc, or
unsafe to silence it. Make illegal states unrepresentable: encode invariants in types so the wrong code fails to
compile instead of failing at runtime. A fight with the borrow checker is a design review the compiler is giving you for
free.
Route to Reference
Read the reference before doing focused work in its area; SKILL.md alone is sufficient for routine code.
- Idioms and ownership patterns —
${CLAUDE_SKILL_DIR}/references/idioms.mdborrowing/ownership restructuring,impl Traitvs generics vsdyn, iterator adapters,match/if let/let else, newtype and builder patterns,From/Into/TryFrom, theas_/to_/into_cost cheat-table - Error handling —
${CLAUDE_SKILL_DIR}/references/errors.mdResult/Option/?,thiserrorenum templates,anyhow.context(), the panic/unwrap/expectpolicy, error-type design (Display/Error/Send/Sync) - Anti-patterns —
${CLAUDE_SKILL_DIR}/references/anti-patterns.mdAI-produced anti-patterns mapped to the Clippy lints that catch them, with the idiomatic fix for each - API design —
${CLAUDE_SKILL_DIR}/references/api-guidelines.mdthe Rust API GuidelinesC-*checklist, RFC 430 naming,#[non_exhaustive],#[must_use], common-trait derives, sealed traits, builders - Parallelism —
${CLAUDE_SKILL_DIR}/references/parallelism.mdthreads, scoped threads, rayon,Send/Sync, channels, shared state (Arc/Mutex) — the CPU-bound multicore path - Async —
${CLAUDE_SKILL_DIR}/references/async.mdtheFuture/executor model, Tokio, the compiler-invisible footgun checklist (blocking in async, holding!Sendacross.await), "do you even need async?" - Project structure —
${CLAUDE_SKILL_DIR}/references/structure.mdmod.rs-free layout, cargo workspaces, the[lints]/[workspace.lints]table, profiles, features, MSRV - Toolchain —
${CLAUDE_SKILL_DIR}/references/toolchain.mdthe fmt/clippy/nextest/deny verification gates, rust-analyzer configuration, recommended compiler and Clippy lint sets - Edition 2024 —
${CLAUDE_SKILL_DIR}/references/edition-2024.mdRust 2024 edition specifics and migration viacargo fix --edition - Documentation —
${CLAUDE_SKILL_DIR}/references/documentation.mdrustdoc conventions, doctests,missing_docs, intra-doc links, semver-checks
Naming
- Casing follows RFC 430:
UpperCamelCasefor types/traits/enum variants,snake_casefor functions/methods/modules,SCREAMING_SNAKE_CASEfor consts/statics. - Acronyms are one word in
UpperCamelCase(Uuid,Stdin, notUUID,StdIn); lower-cased insnake_case. - Getters omit the
get_prefix:fn first(&self) -> &First, notfn get_first. Usegetonly when one obvious value is gotten (e.g.Cell::get). - Ad-hoc conversion methods signal cost through their prefix:
as_= free borrow-to-borrow view;to_= expensive (allocates or computes);into_= consuming, ownership transfer. - Collection iterators are
iter(&T),iter_mut(&mut T),into_iter(T); the iterator type name matches the method (into_iter->IntoIter). - Names use consistent verb-object word order (
ParseAddrError, matchingParseIntError). Drop weasel words —Manager,Service,Factory,Helper,Utilrarely belong in a type name; name the thing for what it is (Bookings, notBookingService), append a quality only when it does something specific (BookingDispatcher). - A
Builderis Rust's name for a factory. Acceptimpl Fn() -> Foorather than aFooBuilderparameter.
Ownership and Borrowing
- Accept the least-owning type that does the job:
&strover&String,&[T]over&Vec<T>,&ToverTwhen not consuming. Return owned values; borrow in parameters. - Treat
.clone()as a signal, not a fix. A clone to satisfy the borrow checker usually means lifetimes or ownership are structured wrong — split the borrow, narrow the scope, or restructure who owns the data. - Reach for
Rc/Arconly for genuine shared ownership (a graph, a cache, a value with no single owner), not to dodge a lifetime annotation. - Prefer adjusting a function to take
&self/&Tover cloning at the call site. Borrow at the narrowest scope so a mutable borrow ends before the next access begins (non-lexical lifetimes allow this). - Use lifetime elision wherever it applies; write explicit lifetimes only when the compiler asks or when they document a real relationship between inputs and outputs.
Error Handling
- Functions that can fail return
Result<T, E>; absence-not-error returnsOption<T>. Propagate with?, do not match-and-rewrap. - Libraries define typed errors with
thiserror(one enum,#[error("...")]per variant,#[from]for source conversions). Applications useanyhow::Resultand attach.context(...)at each layer. - Error types implement
std::error::Error,Display,Debug, and areSend + Sync + 'staticso they cross threads and compose withanyhow/Box<dyn Error>. .unwrap()and.expect()are permitted only on a provable invariant (with.expect("why this cannot fail")documenting the proof), insideconstcontexts, or in tests. They are never error-propagation.- A detected programming bug panics; it is not an
Error. A panic means "stop the program" — never use panics to communicate recoverable errors upstream, and never assume a panic will be caught. Genuinely fallible operations (parsing, I/O, user input) returnResult. - Prefer making invalid states uncompilable over validating at runtime: a
NonEmptyVecor a parsedEmailnewtype beats a runtime check on a rawVec/String.
Traits and Generics
- Static dispatch is the default: generics (
fn f<T: Trait>) orimpl Traitin argument and return position. Monomorphization keeps it allocation-free and inlinable. - Use
dyn Traitonly for genuine runtime polymorphism — heterogeneous collections (Vec<Box<dyn Draw>>), plugin boundaries, or breaking deep monomorphization. Always writedynexplicitly (Box<dyn Error>,&dyn Handler); bare-trait-object syntax has been an error since edition 2021. impl Traitin argument position is shorthand for an anonymous generic; in return position it hides a concrete type. Switch to a named generic when the caller must name the type or supply it via turbofish.- Derive the common traits eagerly where they fit:
Debugon every public type, plusClone,Copy,PartialEq,Eq,Hash,PartialOrd,Ord,Defaultas semantics allow. ImplementDisplayfor types meant to be read by humans (errors, string-like wrappers). - Implement conversions via the standard traits —
From(which givesIntofree),TryFrom,AsRef,AsMut— not ad-hoc methods, so generic code and?interoperate.
Iterators and Pattern Matching
- Express transformations as iterator adapter chains (
.iter().filter().map().collect()), not manual index loops. They are bounds-check-friendly, lazy, and clearer. - Reach for
for x in &collectionoverfor i in 0..collection.len(); index only when the index itself is the data. matcharms must be exhaustive — let the compiler enforce that every variant is handled rather than ending with a catch-all_that silently swallows new variants.- Use
if let/while letfor single-pattern matches, andlet ... else { return / continue / bail }to bind-or- bail in one line instead of nesting the happy path inside amatch. - Destructure in the pattern (function params,
let, match arms) rather than reaching into fields by.0/.fieldafterward.
Concurrency: Choose the Right Model First
Pick the concurrency model from the workload before writing any concurrent code — this decision is not interchangeable:
- CPU-bound / multicore compute (parsing, hashing, image processing, number crunching) -> OS threads or rayon
(
par_iter). Never reach for async here — async provides zero parallel speedup for CPU work; it only interleaves tasks at.awaitpoints on (often) a single logical flow. - Many concurrent I/O operations (thousands of sockets, requests, file handles) -> async / Tokio. Async wins by not blocking a thread while waiting on I/O, letting one runtime juggle huge numbers of in-flight operations.
- A handful of blocking I/O tasks -> plain threads are simpler than dragging in an async runtime; use async only when the concurrency count makes a thread-per-task model wasteful.
Further rules once the model is chosen:
- Shared state crosses threads via
Arc<Mutex<T>>/Arc<RwLock<T>>; immutable shared data viaArc<T>. Move work and ownership across threads with channels (std::sync::mpsc,crossbeam, ortokio::syncin async). - Public types should be
Send(and usuallySync) so they work under Tokio and behind runtime abstractions. A!Sendvalue held across an.awaitmakes the whole future!Send— keepRc/RefCellout of async scopes that span an await. - Never block in async: no
std::thread::sleep, no synchronous file/network I/O, no long CPU loops on the runtime. Offload CPU work withtokio::task::spawn_blockingor a rayon pool, and use the runtime's async timers/IO. - The borrow checker does not see deadlocks or blocking-in-async — those footguns are invisible at compile time. Read
references/async.mdbefore writing async code.
Project Structure
- Use the
mod.rs-free layout: a modulefoolives infoo.rs, its submodules infoo/bar.rs. Never create newmod.rsfiles. - Set all lint levels in the
[lints](or[workspace.lints]) table inCargo.toml, not in scattered#![deny(...)]attributes orRUSTFLAGS. This keeps lint policy in one auditable place and inherited across the workspace. - Override a project-global lint at a narrow scope with
#[expect(lint, reason = "...")], not#[allow]—expectwarns when the lint stops firing, preventing stale overrides. (#[allow]is acceptable on generated code/macros.) - Multi-crate projects use a cargo workspace with a shared
[workspace.lints]and[workspace.dependencies]. Split a crate when a submodule is independently useful — many small crates compile faster and prevent dependency cycles. - Keep
unsafeconfined to the smallest possible module; soundness boundaries are module boundaries. Everyunsafeblock carries a// SAFETY:comment justifying it. Never write sound-looking safe wrappers over unsound assumptions.
Toolchain Gates
Code is not done until these pass (run via Bash, surfacing each command):
cargo fmt --check— formatting is non-negotiable; never hand-format.cargo clippy --all-targets --all-features -- -D warnings— Clippy clean across the whole build graph, warnings as errors. Fix the cause; suppress a lint only with a scoped#[expect(..., reason = "...")].cargo nextest runandcargo test --doc— nextest is the fast test runner but does not execute doctests, so the separate--docrun is mandatory to keep documentation examples honest.cargo deny check(orcargo audit) — gate dependencies for advisories, license, and source policy.
Doc Comments
//!documents the crate root and modules;///documents items (functions, types, fields, variants). Write docs as part of the change, not after.- Use the standard rustdoc sections:
# Examples,# Panics(when the fn can panic),# Errors(what aResultreturns on failure),# Safety(the invariants a caller of anunsafefn must uphold). - Doctests use
?for fallible calls (wrap the body so it returnsResult), never.unwrap()— examples are also documentation of idiomatic usage. - Libraries enable
#![warn(missing_docs)]so every public item is documented. Link related items with intra-doc links ([Type],[mod::func]). - When you change an exported symbol's signature or behavior, update its doc comment in the same edit — a stale doc is worse than none.
Code Navigation (LSP Required)
A rust-analyzer LSP is configured for .rs files. For semantic navigation, use LSP tools — they understand types,
traits, and macro expansion in ways text search cannot:
- goToDefinition — jump to where a symbol is defined (through re-exports and macros).
- findReferences — every real use of a symbol, not textual name collisions.
- hover — resolved type, signature, and docs at a position.
- workspaceSymbol — locate a type/fn/trait by name across the workspace.
- goToImplementation — concrete implementors of a trait, or the trait a method satisfies.
- incomingCalls / outgoingCalls — call hierarchy for impact analysis before a change.
Use Grep/Glob only for non-semantic text: comments, string literals, config files, log messages, Cargo.toml entries.
Reaching for Grep to find a definition or all callers is a mistake when the LSP resolves it precisely.
Integration
- the-coder owns the language-agnostic workflow — discovery, planning, verification loop. This skill governs
Rust-specific conventions only; defer the workflow mechanics to
the-coderand apply these rules within it. - rust-testing owns the test ecosystem (
#[test]/integration/doctest layout, cargo-nextest, proptest, insta, criterion, mockall, rstest). When writing or reviewing tests, that skill leads; it relies on this skill for general language conventions and onreferences/async.mdfor async-test mechanics. - When this skill's conventions conflict with an established pattern already pervasive in the codebase, the codebase wins — follow it and flag the divergence once.
Application
When writing Rust:
- Apply every convention above silently — do not narrate "per RFC 430 I'll name this...". Just write idiomatic code.
- Match the surrounding codebase. If it diverges from these conventions consistently (e.g. uses
mod.rs, a different error crate), follow the codebase and note the divergence once rather than fighting it file-by-file. - Run the toolchain gates before declaring the work done.
When reviewing Rust:
- Cite the specific violation (file:line and the rule), then show the corrected code inline. Do not lecture in the abstract.
- Prioritize correctness and soundness (unsound
unsafe, blocking-in-async, panics-as-errors, data races) over style nits; letcargo fmtand Clippy carry the mechanical issues. - Confirm the concurrency model fits the workload (CPU-bound vs I/O-bound) before reviewing the concurrency details.
The borrow checker is a collaborator, not an adversary. When it pushes back, the answer is almost never .clone(),
Rc, or unsafe — it is a clearer ownership structure. Encode invariants in types so wrong code does not compile, keep
unsafe rare and justified, and let the toolchain gates (fmt, clippy, nextest + doctests, deny) be the final word on
done.