Claude
Skills
Sign in
Back

Rust

Included with Lifetime
$97 forever

This skill should be used when the user asks to "write Rust code", "create a Rust module", "implement a struct", "define a trait", "add error handling", "fix this Rust code", "refactor this Rust", "model this type in Rust", "use async/await", "write an async function", "handle lifetimes", "organize Rust modules", "set up a Rust project", "set up a Cargo workspace", "use serde", "configure tokio", "write Rust tests", or any task involving writing, reviewing, or improving Rust code. Provides best practices for the 2024 edition including type design, error handling, async patterns, trait design, lifetimes, module organization, and testing.

Design

What this skill does


# Rust

Best practices for writing idiomatic Rust targeting the **2024 edition** (Rust 1.85+). Covers type design, error handling, async patterns, trait design, ownership and lifetimes, module organization, and testing.

## Edition and Project Setup

### Cargo.toml

Target the 2024 edition in new projects:

```toml
[package]
edition = "2024"
rust-version = "1.85"
```

### Key 2024 Edition Changes

Changes that affect how code is written day-to-day:

| Change | Impact |
|--------|--------|
| RPIT lifetime capture | `impl Trait` return types capture all in-scope lifetime params by default. Use `use<'a>` to opt out. |
| Unsafe extern blocks | `extern "C" { ... }` items are implicitly unsafe. Wrap in `unsafe extern` or add `safe` keyword to individual items. |
| `unsafe_op_in_unsafe_fn` warning | Unsafe operations inside `unsafe fn` now warn without an inner `unsafe` block. |
| `gen` keyword reserved | Rename any identifiers named `gen` (use `r#gen` as escape hatch). |
| Prelude additions | `Future` and `IntoFuture` are in the prelude — no import needed. |
| Temporary scope changes | `if let` and tail expression temporaries drop before local variables. |
| Cargo MSRV-aware resolver | Cargo prefers dependency versions compatible with declared `rust-version`. |

### Migration

Run `cargo fix --edition` before updating `edition = "2024"` in Cargo.toml. For large codebases, enable `rust-2024-compatibility` lints incrementally. Update code-generating crates (`bindgen`, `cxx`, proc-macros) first.

## Type Design

### Newtype Pattern

Wrap primitive types to create domain-specific types with zero runtime cost:

```rust
struct UserId(u64);
struct EmailAddress(String);

impl UserId {
    fn new(id: u64) -> Self { Self(id) }
    fn as_u64(&self) -> u64 { self.0 }
}
```

Newtypes prevent accidental interchange (`UserId` vs bare `u64`), enable trait implementations on foreign types (orphan rule workaround), and hide internal representation.

### Discriminated Enums

Model variants with data as enums, not stringly-typed fields:

```rust
enum ConnectionState {
    Disconnected,
    Connecting { attempt: u32 },
    Connected { session_id: String },
    Failed { error: String, retries: u32 },
}
```

Match exhaustively. The compiler enforces handling of every variant.

### Typestate Pattern

Encode state transitions in the type system to make invalid states unrepresentable:

```rust
struct Unvalidated;
struct Validated;

struct Form<State> {
    data: FormData,
    _state: std::marker::PhantomData<State>,
}

impl Form<Unvalidated> {
    fn validate(self) -> Result<Form<Validated>, ValidationError> { /* ... */ }
}

impl Form<Validated> {
    fn submit(self) -> Result<Response, SubmitError> { /* ... */ }
}
```

`submit()` is only callable on `Form<Validated>` — calling it on an unvalidated form is a compile error. Use sparingly; the complexity cost is justified when invalid state transitions cause serious bugs.

### Builder Pattern

Use the builder pattern for types with many optional fields:

```rust
struct ServerConfig {
    host: String,
    port: u16,
    max_connections: Option<usize>,
    timeout: Option<Duration>,
}

impl ServerConfig {
    fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
        ServerConfigBuilder {
            host: host.into(),
            port,
            max_connections: None,
            timeout: None,
        }
    }
}

impl ServerConfigBuilder {
    fn max_connections(mut self, n: usize) -> Self {
        self.max_connections = Some(n);
        self
    }

    fn timeout(mut self, duration: Duration) -> Self {
        self.timeout = Some(duration);
        self
    }

    fn build(self) -> ServerConfig {
        ServerConfig {
            host: self.host,
            port: self.port,
            max_connections: self.max_connections,
            timeout: self.timeout,
        }
    }
}
```

Required parameters go in the builder constructor. Optional parameters are set via chained methods consuming and returning `Self`. Call `.build()` to produce the final type. For critical state ordering, combine with the typestate pattern.

For detailed type patterns including generic newtypes, sealed traits, and advanced typestate, consult `references/type-patterns.md`.

## Error Handling

### Library Errors: `thiserror`

Define typed errors at library/crate boundaries:

```rust
use thiserror::Error;

#[derive(Debug, Error)]
enum StorageError {
    #[error("record not found: {id}")]
    NotFound { id: String },
    #[error("connection failed")]
    Connection(#[from] std::io::Error),
    #[error("deserialization failed")]
    Deserialize(#[source] serde_json::Error),
}
```

`#[from]` generates `From` impls for automatic `?` conversion. `#[source]` preserves the error chain without generating `From`. Always derive `Debug`.

### Application Errors: `anyhow`

Use `anyhow` at the application layer for ergonomic error aggregation:

```rust
use anyhow::{Context, Result};

fn load_config(path: &Path) -> Result<Config> {
    let contents = std::fs::read_to_string(path)
        .context("failed to read config file")?;
    let config: Config = toml::from_str(&contents)
        .context("failed to parse config")?;
    Ok(config)
}
```

Always add `.context()` when propagating with `?` — bare `?` loses the "what was happening" information.

### Guidelines

| Layer | Crate | Pattern |
|-------|-------|---------|
| Library / public API | `thiserror` | Named error enum with `#[from]` / `#[source]` |
| Application / `main` | `anyhow` | `anyhow::Result<T>` with `.context()` |
| Internal modules | Either | Match the layer's convention |

Do not over-engineer error types. If callers always handle variants the same way, fewer variants or `#[error(transparent)]` wrapping is better.

## Async Patterns

### `async fn` in Traits (Stable since 1.75)

```rust
trait DataStore {
    async fn get(&self, key: &str) -> Result<Vec<u8>>;
    async fn put(&self, key: &str, value: &[u8]) -> Result<()>;
}
```

Caveat: async trait methods are not `dyn`-compatible. Use the `async-trait` crate or manual `Pin<Box<dyn Future>>` desugaring when trait objects are needed.

### Async Closures (Stable since 1.85)

```rust
let fetch = async |url: &str| {
    reqwest::get(url).await?.text().await
};
```

Async closures return futures when called. The compiler uses `AsyncFn`, `AsyncFnMut`, and `AsyncFnOnce` traits internally — pass async closures directly rather than naming these traits in bounds.

### Cancellation and Timeouts

```rust
use tokio::time::{timeout, Duration};

let result = timeout(Duration::from_secs(5), some_async_work()).await;
match result {
    Ok(inner) => { /* inner is the Result from some_async_work */ }
    Err(_) => { /* timed out */ }
}
```

Use `tokio::select!` for racing multiple futures. Use `CancellationToken` for structured cancellation across task trees.

### Send + Sync Rules

`tokio::spawn` requires `Future + Send + 'static`. Common fixes for `Send` errors:

- **Don't hold `MutexGuard` across `.await`** — drop the guard before awaiting.
- **Use `tokio::sync::Mutex`** when a lock must span an `.await` point.
- **Use `spawn_local`** for futures that cannot be `Send`.

### Blocking Work

Never run CPU-intensive or blocking I/O on the async runtime. Use `tokio::task::spawn_blocking` for blocking operations and `tokio::sync::mpsc` channels to communicate results back.

For detailed tokio channel types, `select!` patterns, and task management, consult `references/api-reference.md`.

## Trait Design

### Associated Types vs. Generics

Use **associated types** when there is one natural type per implementation:

```rust
trait Parser {
    type Output;
    fn parse(&self, input: &str) -> Result<Self::Output>;
}
```

Use **generic parameters** when a single type can implement the trait for multiple type arguments:

```rust
trait Convert<T> {
    fn convert(&self) -> T;
}
```

### Sealed Traits

Seal traits when implementations should only exist inside th
Files: 3
Size: 31.5 KB
Complexity: 55/100
Category: Design

Related in Design