Rust best practices
Skill adulari/forge/crates/forge-skills/builtin/rust-best-practices
Fast, model-agnostic AI coding harness and CLI in Rust — routes every task to the optimal model for cost × capability.
npx -y skills add adulari/forge --skill rust-best-practicesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 8 stars8 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Write, review, or refactor Rust so it is idiomatic, safe, and maintainable. Use this skill whenever the task involves Rust code — implementing a feature in a `.rs` file, reviewing a Rust PR or module, refactoring a crate, designing a public API, fixing clippy or borrow-checker complaints, deciding how to structure error handling, or splitting an oversized function or file. Trigger even when the user does not say "best practices" explicitly: any request to "write this in Rust", "clean up this Rust code", "is this idiomatic", "add a Cargo crate", "handle these errors properly", or "make this compile without unwrap" should pull in this skill. Covers ownership and borrowing, error handling (Result / `?` / thiserror / anyhow), API design, module and crate layout, size limits, unsafe, dependencies, testing, and the verification steps (fmt, clippy, test) that must pass before the work is called done.
SKILL.md
9.4 KB, as published. Nobody here has run it
Rust Best Practices
Idiomatic Rust is not a style preference — it is how you get the compiler and the
ecosystem to catch your mistakes for you. The goal of every guideline here is to move a
class of bug from "found in production" to "found at compile time" or "found by
cargo clippy". When you apply a rule, understand which failure it prevents; that lets
you know when the rule genuinely doesn't apply.
Before you write code
Read the surrounding module first. A crate has a texture — its error type, its naming, how it splits modules, whether it leans on iterators or explicit loops. Match it. A locally "more correct" pattern that fights the existing conventions is worse than a consistent one, because the next reader now has to hold two mental models.
The verification gate (non-negotiable)
Rust code is not done until these pass. Run them and fix what they report — do not report success on unverified code.
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo fmt removes all whitespace and layout debate. clippy with -D warnings is the
single highest-leverage habit in Rust: it catches needless clones, non-idiomatic patterns,
likely bugs, and performance traps before review. If a lint is genuinely wrong for a case,
#[allow(clippy::x)] it locally with a one-line reason — never blanket-allow at crate
level, which silences future real hits.
Error handling
This is where most non-idiomatic Rust shows up. The rules:
- Return
Result, propagate with?. Don'tmatchaResultjust to re-return it.?is the idiom; it also converts error types throughFrom, which is the whole point. unwrap/expect/panic!are for "this cannot fail and if it does the program is broken". In library code and any path driven by external input, they are bugs waiting to happen.expect("reason")beatsunwrap()because the message documents the invariant and shows up in the panic. Reserve bareunwrap()for tests and truly impossible cases.- Applications vs libraries pick different error crates:
- Application / binary:
anyhow::Result<T>with.context("what failed")at each boundary. You want a readable error chain, not a typed taxonomy the caller matches on. - Library: a typed error enum via
thiserror, so callers can match on variants and your error isstd::error::Error. Libraries should not forceanyhowon their users.
- Application / binary:
- Errors are meaningful and well-behaved (Rust API Guideline C-GOOD-ERR): implement
Debug,Display, andError; carry enough context to act on; never stringify away structure the caller needs. - Don't swallow errors with
let _ =or.ok()unless ignoring them is genuinely correct — and if it is, a short comment says why.
Ownership, borrowing, and types
- Borrow, don't clone, to satisfy the borrow checker. A
.clone()added only to make an error go away is a smell — usually the fix is&, a lifetime, or restructuring. Clone when you genuinely need an owned copy, not as a borrow-checker escape hatch. - Accept the most general type; return the most specific. Take
&strnot&String,&[T]not&Vec<T>,impl AsRef<Path>not&PathBuf. Return concrete types the caller can use directly. - Make illegal states unrepresentable. Encode invariants in the type system: a newtype
(
struct UserId(u64)) instead of a bareu64; anenuminstead of abool+Optioncombo that has meaningless states. Arguments should convey meaning through types, not positionalbools (API Guideline C-CUSTOM-TYPE). - Prefer iterators and combinators (
map,filter,collect,?-in-iterator viacollect::<Result<_,_>>()) over manual index loops — they are clearer and eliminate off-by-one and bounds bugs. Don't force it, though: a plainforloop with side effects is fine when a combinator chain would be more obscure. - Derive the common traits (
Debug,Clone,PartialEq, andEq/Hash/Default/Copywhere they make sense). Every public type should implementDebug(C-DEBUG).
API design (public interfaces)
- Follow the naming conventions:
as_/to_/into_for conversions by cost,iter/iter_mut/into_iterfor iterators, RFC 430 casing. - Keep struct fields private and expose behavior through methods, so you can change internals without a breaking release (C-STRUCT-PRIVATE).
- Use the builder pattern for types with many optional fields rather than a telescoping
constructor or a giant
Option-fillednew. - Implement standard conversion traits (
From,TryFrom,AsRef) instead of ad-hocto_x/from_xfree functions;Fromgives youIntofor free and plugs into?. - Document every public item with a rustdoc comment, and include a
# Errorssection for fallible functions and a# Panicssection for ones that can panic. Examples in docs are compiled and tested — they double as regression tests.
Size and structure
These are conventions, not compiler limits — but they track real maintainability, and clippy enforces the function one by default.
| Scope | Practical target | Basis |
|---|---|---|
| Function / method | ≤ 100 lines | clippy too_many_lines default threshold is 100. |
.rs file / module | ≤ ~1,000 lines | No compiler/clippy limit; a common team convention. Split by responsibility. |
| Crate | No line cap | Split around a real API / domain / dependency boundary, not a line count. |
Published .crate | ≤ 10 MB compressed | crates.io hard limit on the packaged archive. |
Exceptions that legitimately blow past the file target: generated code, large lookup
tables/fixtures, and macro-heavy modules. When a function crosses ~100 lines, the fix is
almost always to extract a well-named helper, not to #[allow] the lint — the name you give
the extracted piece is documentation.
Organize modules by responsibility, not by type-kind (mod users beats a mod structs).
Keep the crate's public surface in lib.rs/mod.rs re-exports so callers have one obvious
import path.
Unsafe
- Default to safe Rust. Reach for
unsafeonly for FFI, genuine performance-critical spots proven by measurement, or building a safe abstraction over a raw operation. - Every
unsafeblock gets a// SAFETY:comment stating the invariant that makes it sound. Keep unsafe blocks minimal and wrapped in a safe API so the unsafety doesn't leak.
Dependencies
- Add a dependency when it earns its keep; each one is compile time, audit surface, and a future upgrade. Prefer well-maintained, widely-used crates.
- Use precise-enough version requirements, keep
Cargo.lockcommitted for binaries, and runcargo updatedeliberately. Considercargo deny/cargo auditfor license and vulnerability checks on anything shipped. - Fill in
Cargo.tomlmetadata for published crates (description, license, repository, keywords, categories) — C-METADATA.
Testing
- Unit tests live in a
#[cfg(test)] mod testsbeside the code; integration tests go intests/. Test behavior and edge cases, not just the happy path. - Prefer table-driven tests for multiple input/output cases. Use
assert_eq!with meaningful values so failures are legible. - Doctests keep examples honest — put a runnable example on public functions.
- For parsing/serialization and anything with an input space, consider property tests
(
proptest/quickcheck).
Async (when it applies)
- Don't block in async code (
std::fs,std::thread::sleep, long CPU loops) — it stalls the executor. Use the async equivalents orspawn_blocking. - Don't hold a
std::sync::Mutexguard across an.await. Use an async-aware lock or drop the guard first. - Don't add
asyncto a function that never awaits.
Applying this in a review
When reviewing Rust rather than writing it, scan in this order — it surfaces the highest-value
issues first: (1) unwrap/expect/panic! on fallible or input-driven paths; (2) error
handling that discards context or matches-to-re-return instead of ?; (3) needless clones
and overly-specific parameter types; (4) missing Debug/derives on public types and missing
docs on public items; (5) oversized functions that should be split; (6) any unsafe without a
// SAFETY: justification. Point to path:line, say which failure the issue invites, and
prefer suggesting the idiomatic shape over just naming the rule.