Skip to content
Back to skills

Verbose

ASecurity

Rust splits failure into two categories. `Result<T, E>` represents conditions a caller can reasonably anticipate and recover from — missing files, malformed input, a rejected request. `panic!` represents a bug: a violated invariant that means the program is in a state its author never intended.

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 10, 2026
code-qualityrustgoreactdebuggingcode-reviewapi

Works with

  • api

Security analysis

A100/100

Scanned September 10, 2026

npx -y skills add snoodleboot-io/prompticorn --skill verbose --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Verbose?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Verbose
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/snoodleboot-io-verbose-001da57b/badge)](https://www.skillsdirectory.com/skills/snoodleboot-io-verbose-001da57b)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
# Rust Error Handling Patterns (Verbose)

## Core Patterns

### Result vs Panic: the Recoverable/Bug Split

Rust splits failure into two categories. `Result<T, E>` represents conditions a
caller can reasonably anticipate and recover from — missing files, malformed
input, a rejected request. `panic!` represents a bug: a violated invariant that
means the program is in a state its author never intended.

```rust
use std::num::ParseIntError;

// Recoverable — the caller decides what to do with a bad value.
fn parse_age(input: &str) -> Result<u8, ParseIntError> {
    input.trim().parse::<u8>()
}

// Bug — a positive worker count is an invariant of a valid config.
fn spawn_workers(count: usize) {
    assert!(count > 0, "worker count must be positive, got {count}");
    // ...
}
```

The test: could a well-written caller do something sensible on failure? If yes,
return `Result`. If the only correct response is "fix the code," panic.

### The `?` Operator

`?` is the propagation primitive. On `Ok(v)` it evaluates to `v`; on `Err(e)` it
returns `Err(e.into())` from the enclosing function. It works on `Option` too,
returning `None` early.

```rust
use std::fs::File;
use std::io::{self, Read};

fn read_username() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("user.txt")?.read_to_string(&mut s)?;   // two early-return points
    Ok(s.trim().to_string())
}
```

The hidden `.into()` in `?` is the mechanism behind unified error types: as long
as the source error implements `From` into the function's error type, `?`
converts it automatically.

### Converting Errors With `From`

A function that can fail in several ways declares one error type and provides a
`From` impl per source. Then `?` converts each at the call site.

```rust
use std::fmt;

#[derive(Debug)]
enum ConfigError {
    Read(std::io::Error),
    Parse(std::num::ParseIntError),
}

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ConfigError::Read(e) => write!(f, "reading config: {e}"),
            ConfigError::Parse(e) => write!(f, "parsing config: {e}"),
        }
    }
}
impl std::error::Error for ConfigError {}

impl From<std::io::Error> for ConfigError {
    fn from(e: std::io::Error) -> Self { ConfigError::Read(e) }
}
impl From<std::num::ParseIntError> for ConfigError {
    fn from(e: std::num::ParseIntError) -> Self { ConfigError::Parse(e) }
}

fn max_connections(path: &str) -> Result<u32, ConfigError> {
    let raw = std::fs::read_to_string(path)?;   // io::Error -> ConfigError
    let n = raw.trim().parse::<u32>()?;          // ParseIntError -> ConfigError
    Ok(n)
}
```

This is exactly the boilerplate `thiserror` eliminates.

### thiserror for Library Errors

Libraries should expose typed errors so downstream code can match on kinds and
react. `thiserror` derives `Display`, `Error`, and the `From` conversions from
attributes — the hand-written enum above collapses to this:

```rust
#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
    #[error("reading config: {0}")]
    Read(#[from] std::io::Error),          // #[from] generates the From impl

    #[error("parsing config: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("missing required key: {key}")]
    MissingKey { key: String },            // struct variant with named field
}
```

Callers get a stable, exhaustively-matchable API:

```rust
match max_connections("app.toml") {
    Ok(n) => start(n),
    Err(ConfigError::MissingKey { key }) => eprintln!("add {key} to app.toml"),
    Err(e) => eprintln!("config invalid: {e}"),
}
```

### anyhow for Application Errors

Application code at the top of the stack usually does not match on error kinds —
it logs the failure with context and exits or returns an HTTP 500. `anyhow`
provides a single boxed error type plus a `.context()` combinator that builds a
readable causal chain.

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

fn load_profile(user_id: u64) -> Result<Profile> {
    let path = format!("profiles/{user_id}.json");
    let raw = std::fs::read_to_string(&path)
        .with_context(|| format!("reading profile {user_id}"))?;
    let profile: Profile = serde_json::from_str(&raw)
        .with_context(|| format!("parsing profile {user_id}"))?;
    Ok(profile)
}
```

A failure prints as a chain: `reading profile 42: No such file or directory`.
Rule of thumb: `thiserror` in libraries, `anyhow` in binaries and application
layers. Do not put `anyhow::Error` in a library's public signatures — it robs
callers of the ability to match.

### Option Combinators

Reach for combinators before `match` when transforming optional values; they keep
the happy path linear and the intent obvious.

```rust
fn port_or_default(cfg: &Config) -> u16 {
    cfg.get("port")
        .and_then(|s| s.parse::<u16>().ok())   // Option<&str> -> Option<u16>
        .filter(|&p| p != 0)
        .unwrap_or(8080)
}
```

Bridge the two worlds with `ok_or` / `ok_or_else` (Option → Result) and `.ok()`
(Result → Option, discarding the error):

```rust
let name = cfg.get("name").ok_or(ConfigError::MissingKey { key: "name".into() })?;
```

## Common Anti-Patterns

❌ **`String` as the error type** — unmatchable, and it discards the source error.
```rust
fn load() -> Result<Config, String> {
    std::fs::read_to_string("c.toml").map_err(|e| e.to_string())?;   // lossy
    // ...
}
```
✅ **A typed error (thiserror) preserving the source via `#[from]`.**
```rust
fn load() -> Result<Config, ConfigError> {
    let raw = std::fs::read_to_string("c.toml")?;   // source kept in ConfigError::Read
    // ...
}
```

❌ **`unwrap()` on fallible I/O in production** — one bad file crashes the process.
```rust
let cfg = std::fs::read_to_string("c.toml").unwrap();
```
✅ **Propagate, or `expect` with a proof of why it cannot fail.**
```rust
let cfg = std::fs::read_to_string("c.toml")?;
// or, for a compile-time constant that genuinely always parses:
let port: u16 = "8080".parse().expect("literal 8080 is a valid u16");
```

❌ **`anyhow::Error` in a library's public API**, forcing every caller to treat all
failures as opaque.
✅ **Expose a `thiserror` enum; let the application layer wrap it in `anyhow`.**

❌ **Silently swallowing errors.**
```rust
let _ = write_audit_log(&event);   // failure vanishes
```
✅ **Handle or log it; if truly ignorable, say why.**
```rust
if let Err(e) = write_audit_log(&event) {
    tracing::warn!("audit log dropped: {e}");
}
```

❌ **`panic!` for input validation the caller could handle.**
✅ **Return `Err`; reserve panics for invariants that indicate a bug.**

## Error Handling Checklist

- [ ] Recoverable failures return `Result`; panics reserved for bugs and broken invariants
- [ ] Error propagation uses `?`, not manual `match` + early `return`
- [ ] Library crates expose a typed error enum (via `thiserror`), not `String` or `anyhow`
- [ ] Each source error converts through `From` / `#[from]`, preserving the cause
- [ ] Applications use `anyhow` with `.context()` for readable failure chains
- [ ] `unwrap`/`expect` appear only in tests, examples, or provably-infallible spots
- [ ] Every `expect` message states the invariant that makes failure impossible
- [ ] Optional values use combinators (`map`/`and_then`/`ok_or`) over nested matching
- [ ] No error is silently discarded without a stated reason

See `code-review-practices` for reviewing error paths and `debugging-methodology`
for turning an error chain into a root cause.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…