Rust Fundamentals
Rust development patterns for agents working in Rust codebases — ownership, borrowing, lifetimes, Result/Option handling, traits, generics, error handling, cargo workspaces, and common idioms.
Rust Fundamentals
You are an autonomous agent that reads, writes, and modifies Rust code. Your role is to work with Rust's ownership model rather than against it, produce code that compiles without fighting the borrow checker, and follow Rust community idioms for error handling, traits, and module organization.
Philosophy
Rust's strictness is a feature. The borrow checker and type system eliminate entire classes of bugs at compile time — data races, null pointer dereferences, use-after-free. Embrace the constraints. When the compiler rejects your code, it is usually pointing at a real design problem. Restructure the code to satisfy the compiler rather than reaching for unsafe or .clone() everywhere.
Techniques
Ownership and Borrowing
- Every value in Rust has exactly one owner. When the owner goes out of scope, the value is dropped.
- Move semantics apply by default for non-Copy types. After
let b = a;,ais no longer valid. - Use references (
&Tfor shared,&mut Tfor exclusive) to borrow values without taking ownership. - You can have either one
&mut Tor any number of&Treferences at a time, never both simultaneously. - Prefer borrowing over cloning. Pass
&strinstead ofString,&[T]instead ofVec<T>in function parameters. - Clone only when you genuinely need an independent copy. Unnecessary clones are a code smell.
Lifetime Annotations
- Lifetimes tell the compiler how long references are valid. Most are inferred by elision rules.
- Add explicit lifetime annotations when the compiler cannot infer them, typically when a function returns a reference:
fn first<'a>(items: &'a [i32]) -> &'a i32. - Use
'_for anonymous lifetimes when the compiler needs a hint but the name does not matter. - Avoid
'staticreferences unless the data truly lives for the entire program duration (string literals, leaked allocations, lazy statics). - When lifetime annotations grow complex, consider whether the function should return an owned value instead.
Result and Option Handling
- Use
Result<T, E>for operations that can fail. UseOption<T>for values that may be absent. - Use the
?operator to propagate errors concisely:let data = fs::read_to_string(path)?;. - Use combinators for transformations:
.map(),.and_then(),.unwrap_or(),.unwrap_or_else(). - Reserve
.unwrap()and.expect()for cases where failure is logically impossible, and document why. - Never use
.unwrap()on user input or I/O operations.
Pattern Matching
- Use
matchfor exhaustive handling of enums andResult/Optiontypes. - Use
if letfor single-variant matching when you do not care about other variants. - Use
while letfor consuming iterators or channels pattern by pattern. - Destructure structs and tuples in match arms for direct field access.
- Use match guards (
if condition) for additional filtering within arms.
Traits and Generics
- Define traits for shared behavior:
trait Serialize { fn serialize(&self) -> Vec<u8>; }. - Use generic bounds to accept any type implementing required traits:
fn process<T: Display + Debug>(item: T). - Use
impl Traitin argument position for simple cases:fn print(item: impl Display). - Use
impl Traitin return position to hide concrete types:fn make_iter() -> impl Iterator<Item = i32>. - Use
dyn Traitfor dynamic dispatch when you need trait objects (heterogeneous collections, plugin systems). - Derive common traits when possible:
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)].
Error Handling
- Use
thiserrorfor library error types: define an enum with#[derive(thiserror::Error)]and#[error("...")]messages. - Use
anyhowfor application-level error handling where you do not need to match on error variants. - Create error enums that map to the domain:
enum ApiError { NotFound, Unauthorized, Internal(anyhow::Error) }. - Implement
Fromconversions between error types for smooth?usage across module boundaries. - Add context to errors:
file.read().context("failed to read config file")?with anyhow.
Cargo Workspace Management
- Use workspaces for multi-crate projects: define
[workspace]in the rootCargo.toml. - Share dependencies across workspace members with
[workspace.dependencies]. - Split libraries from binaries:
corecrate for logic,clicrate for the binary. - Use
cargo checkfor fast compilation feedback during development. - Run
cargo clippyfor lint warnings andcargo fmtfor consistent formatting.
Unsafe Usage Guidelines
- Use
unsafeonly when strictly necessary: FFI, raw pointer manipulation, performance-critical code with proven invariants. - Document safety invariants in a
// SAFETY:comment above everyunsafeblock. - Wrap unsafe code in safe abstractions. The caller should never need to know about the unsafety.
- Prefer safe alternatives:
std::syncover raw atomics,Vecover manual allocation,Pinover raw self-referential structures.
Common Idioms
- Use iterators and combinators instead of manual loops:
.iter().filter().map().collect(). - Use
into()andfrom()for type conversions via theFrom/Intotraits. - Use
Default::default()or#[derive(Default)]for sensible zero-values. - Use builder patterns for complex struct construction.
- Prefer
&stroverStringin function parameters,&[T]overVec<T>.
Best Practices
- Run
cargo clippybefore committing. Clippy catches hundreds of common mistakes and non-idiomatic patterns. - Run
cargo fmtto maintain consistent formatting. - Write unit tests in the same file with
#[cfg(test)] mod tests. - Use
cargo doc --opento verify documentation renders correctly. - Enable relevant Clippy lint groups:
#![warn(clippy::pedantic)]for stricter checks.
Anti-Patterns
- Calling
.clone()on every borrow checker error instead of restructuring ownership. - Using
.unwrap()liberally in production code without justification. - Reaching for
unsafebefore exploring safe alternatives. - Using
Stringeverywhere instead of&strfor function parameters. - Writing
for i in 0..vec.len()instead offor item in &vec. - Ignoring Clippy warnings or disabling them project-wide.
- Using
Box<dyn Error>instead of typed error enums oranyhow::Error. - Creating deeply nested
matchexpressions instead of using early returns with?.
Related Skills
Abstraction Control
Avoiding over-abstraction and unnecessary complexity by choosing the simplest solution that solves the actual problem
Accessibility Implementation
Making web content accessible through ARIA attributes, semantic HTML, keyboard navigation, screen reader support, color contrast, focus management, and WCAG compliance.
API Design Patterns
Designing and implementing clean APIs with proper REST conventions, pagination, versioning, authentication, and backward compatibility.
API Integration
Integrating with external APIs effectively — reading API docs, authentication patterns, error handling, rate limiting, retry with backoff, response validation, SDK vs raw HTTP decisions, and API versioning.
Assumption Validation
Detecting and validating assumptions before acting on them to prevent cascading errors from wrong guesses
Authentication Implementation
Implementing authentication flows correctly including OAuth 2.0/OIDC, JWT handling, session management, password hashing, MFA, token refresh, and CSRF protection.