From cd8e4098edf567f5ea4c8d59b12f1fc00147aa9e Mon Sep 17 00:00:00 2001 From: Max Wase Date: Thu, 7 May 2026 20:55:54 +0200 Subject: [PATCH 1/2] Init skill and claude marketplace --- .claude-plugin/marketplace.json | 16 + .gitignore | 3 + plugins/errortools/.claude-plugin/plugin.json | 23 ++ plugins/errortools/skills/errortools/SKILL.md | 276 ++++++++++++++++++ 4 files changed, 318 insertions(+) create mode 100644 .claude-plugin/marketplace.json create mode 100644 plugins/errortools/.claude-plugin/plugin.json create mode 100644 plugins/errortools/skills/errortools/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..a97cafc --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-marketplace.json", + "name": "errortools-marketplace", + "owner": { + "name": "Max Wase", + "email": "max.vvase@gmail.com" + }, + "description": "Marketplace distributing the errortools Rust error-handling skill", + "plugins": [ + { + "name": "rust-error-handling", + "source": "./plugins/errortools", + "description": "Rust error-handling guidance built around errortools and thiserror crates" + } + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ea8c4bf..ed871df 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ /target +.agents +.claude +.codex diff --git a/plugins/errortools/.claude-plugin/plugin.json b/plugins/errortools/.claude-plugin/plugin.json new file mode 100644 index 0000000..9b1f7d4 --- /dev/null +++ b/plugins/errortools/.claude-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "name": "rust-error-handling", + "description": "Rust error-handling guidance built around errortools and thiserror crates (MainResult, FormatError, custom Format strategies)", + "version": "0.1.0", + "author": { + "name": "Max Wase", + "email": "max.vvase@gmail.com" + }, + "homepage": "https://github.com/maxwase/errortools", + "repository": "https://github.com/maxwase/errortools", + "license": "MIT", + "keywords": [ + "rust", + "error", + "thiserror", + "main", + "format", + "chain", + "source", + "handling" + ], + "$schema": "https://www.schemastore.org/claude-code-plugin-manifest.json" +} \ No newline at end of file diff --git a/plugins/errortools/skills/errortools/SKILL.md b/plugins/errortools/skills/errortools/SKILL.md new file mode 100644 index 0000000..9d4989a --- /dev/null +++ b/plugins/errortools/skills/errortools/SKILL.md @@ -0,0 +1,276 @@ +--- +name: errortools +description: Use when writing or refactoring Rust error-handling code — covers idiomatic source-chain design with `thiserror` and ad-hock error logging. +--- + +# Rust error-handling skill + +Apply this whenever you are designing error types, deciding how `main` returns errors, or formatting an error chain for users or logs in a Rust project. The `errortools` crate ([crates.io](https://crates.io/crates/errortools), [docs.rs](https://docs.rs/errortools)) provides the runtime pieces; this skill encodes the conventions for using it well. + +## When to reach for it + +- Binary's `main` currently does the `if let Err(e) = run() { eprintln!(...); exit(1) }` dance — replace with `MainResult`. +- You see `Error: Outer(Inner(Io(Os { ... })))` in output — that's `Debug` formatting bleeding through; switch to `MainResult` or `FormatError`. +- You need to log a full source chain on one line (structured logs) or as a tree (human terminal). +- You want a project-specific error format — implement `Format` once, reuse via `MainResult` and `e.formatted::()`. + +If the project does not depend on `errortools`, add it to `Cargo.toml`: + +```toml +[dependencies] +errortools = "0.1" +thiserror = "2" +``` + +`errortools` is `no_std`-capable: disable the default `std` feature for embedded targets (`default-features = false`). + +## Core API cheat sheet + +| Item | Purpose | +|---|---| +| `MainResult` | Return type for `fn main`. Renders `E` via `Format` strategy `F` instead of `Debug`. | +| `OneLine` | Default strategy: joins error + sources with `": "`. | +| `Tree` | Indented multi-line strategy with `└──` connectors. | +| `Format` trait | Implement on a unit type to define a custom strategy. | +| `FormatError` ext trait | Adds `.one_line()`, `.tree()`, `.formatted::()` to any `&dyn Error`. | +| `chain(&dyn Error)` | Iterator over the error and its `source()` chain — use inside `Format` impls. | +| `Formatted` | Wrapper whose `Display` runs strategy `F` over `E`. | +| `DisplaySwapDebug` | Swaps `Debug` and `Display`, so returning it from `main` prints the `Display` form. | + +## Patterns + +### Pattern: `MainResult` for binary entrypoints + +```rust +use errortools::MainResult; +use std::{fs, io}; + +#[derive(Debug, thiserror::Error)] +enum Error { + #[error("failed to load config")] + Config(#[source] io::Error), +} + +fn main() -> MainResult { + fs::read_to_string("missing.toml").map_err(Error::Config)?; + Ok(()) +} +``` + +Output: + +```text +Error: failed to load config: No such file or directory (os error 2) +``` + +For tree output, parameterise the strategy: `fn main() -> MainResult`. + +### Pattern: ad-hoc logging mid-function + +When you cannot return — e.g., inside a `tokio::spawn`, an event handler, or a retry loop — use `FormatError`: + +```rust +use errortools::FormatError; + +if let Err(e) = do_thing().await { + tracing::error!("do_thing failed: {}", e.one_line()); +} +``` + +Pick the strategy inline: + +```rust +use errortools::{FormatError, Tree}; +eprintln!("{}", e.formatted::()); +``` + +### Pattern: custom format strategy + +Define once per project, reuse everywhere: + +```rust +use core::{error::Error, fmt}; +use errortools::{Format, FormatError, chain}; +use itertools::Itertools; + +pub struct Arrow; +impl Format for Arrow { + fn fmt(error: &dyn Error, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", chain(error).format(" -> ")) + } +} + +// usage: +// fn main() -> MainResult { ... } +// tracing::error!("{}", e.formatted::()); +``` + +`chain` walks `error.source()` repeatedly — never call `source()` by hand inside a `Format` impl. + +## Error-type discipline + +### Defining the error type + +1. **MUST** derive `thiserror::Error` + `Debug`. One error type per module, named `Error` (used as `feature::Error` from outside). + + ```rust + // GOOD + #[derive(Debug, thiserror::Error)] + pub enum Error { /* … */ } + ``` + +2. **MUST** collapse single-variant enums to structs. + + ```rust + // BAD + pub enum Error { ReadFile(#[source] io::Error) } + // GOOD + pub struct ReadFile(#[source] io::Error); + ``` + +3. **MUST** use a tuple variant when wrapping a foreign error with no extra context. + + ```rust + // GOOD + #[error("Failed to open config")] + ConfigOpen(#[source] io::Error), + ``` + +4. **MUST** use a struct variant when extra context is needed; put context in fields, never inside the message via `format!`. + + ```rust + // BAD + #[error("render template {}", name)] + Render(String, #[source] tera::Error), + // GOOD + #[error("render template {name}")] + Render { name: String, #[source] source: tera::Error }, + ``` + +5. **MUST NOT** print the source inside the variant message — `#[source]` already chains it. `OneLine` / `Tree` walk `source()` and join. + + ```rust + // BAD + #[error("read failed: {0}")] Read(#[source] io::Error), + // GOOD + #[error("read failed")] Read(#[source] io::Error), + ``` + +6. **PREFER** specific variants over generic ones. `&'static str` payloads only when the variant is one-off. + + ```rust + // BAD + Other(String), + // GOOD + #[error("Failed to join task '{0}'")] + TokioJoin(&'static str), + ``` + +### Converting at the call site + +7. **MUST** pass the variant constructor directly to `map_err`. + + ```rust + // BAD + .map_err(|source| Error::Config { source })? + // GOOD + .map_err(Error::Config)? + ``` + +8. **PREFER** chaining through existing variants over inventing new wrapper variants. + + ```rust + // BAD — new top-level variant just to wrap an inner one + #[error("inner")] InnerWrap(#[source] inner::Error), + // GOOD — reuse via From + .ok_or(top::Error::from(inner::Error::Foo(ctx)))? + ``` + +9. **MUST NOT** put `#[from]` on context-less variants. `#[from]` is allowed only when the source error already carries the operation context. + + ```rust + // BAD — every SQL collapses to one variant + #[error("db")] Db(#[from] sqlx::Error), + // GOOD + #[error("load user {id}")] + LoadUser { id: UserId, #[source] source: sqlx::Error }, + ``` + +10. **MUST NOT** hand-write `impl From for Error`. Use `#[source]` or `#[from]` only. + +11. **PREFER** `#[error(transparent)]` + `#[from]` only when the inner error is the whole story (re-export wrappers). + +### Logging mid-flow + +12. **PREFER** `errortools::FormatError` when you cannot return — never walk `source()` by hand. + + ```rust + // BAD + let mut cur: &dyn Error = &e; + while let Some(s) = cur.source() { /* … */ } + // GOOD + use errortools::FormatError; + tracing::error!("do_thing: {}", e.one_line()); + ``` + +### Returning from `main` + +13. **MUST** use `fn main() -> MainResult` (or `MainResult`). The strategy renders the chain via `Display`; `Debug` never reaches stderr. + + ```rust + // BAD + fn main() -> Result<(), Error> { … } + // GOOD + fn main() -> errortools::MainResult { … } + ``` + +14. **MUST** confine `exit(1)` and `panic!()` to `main`. Business logic returns `Result`. + +15. **MUST** do graceful shutdown in `main` (join threads, close connections). **AVOID** calling `drop(v)` manually — rely on scope. + +### Panics + +16. **MUST NOT** `unwrap()` / `expect()` in production or library code. If unavoidable, document it under `# Panics`. + + ```rust + // BAD + let cfg = load().unwrap(); + // GOOD + let cfg = load().map_err(config::Error::Config)?; + ``` + +### Batch operations + +17. **MUST NOT** silently skip failed items unless the API contract says so — fail fast, or collect and report per-item errors. + +### `anyhow` / `Box` + +18. **AVOID** `anyhow` or other dynamic error types outside tests / throwaway scripts. Production code uses explicit `thiserror` enums. + +### Tests + +19. **MUST** assert the exact error variant with arguments, not `.is_err()`. + + ```rust + // BAD + assert!(result.is_err()); + // GOOD + assert!(matches!(result, Err(Error::Config(_)))); + ``` + +## Choosing a format strategy + +| Context | Strategy | +|---|---| +| CLI tools, default | `OneLine` (single tidy line, greppable) | +| Interactive terminals where chains can be deep | `Tree` | +| Structured logs (JSON, OpenTelemetry) | `OneLine` — keep one log line per error | +| Project house style | Custom `Format` impl, applied uniformly | + +Switch globally by changing the type parameter on `MainResult` — there is no need to touch call sites. + +## References + +- README: +- Examples: (`one_line`, `tree`, `format_error`, `custom_format`, `transparent`) +- API docs: From 758a3a6e0a98fef24fb291b57c8eecc9a110c5f4 Mon Sep 17 00:00:00 2001 From: Max Wase Date: Thu, 7 May 2026 21:25:41 +0200 Subject: [PATCH 2/2] typo fix Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- plugins/errortools/skills/errortools/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/errortools/skills/errortools/SKILL.md b/plugins/errortools/skills/errortools/SKILL.md index 9d4989a..e4370d3 100644 --- a/plugins/errortools/skills/errortools/SKILL.md +++ b/plugins/errortools/skills/errortools/SKILL.md @@ -1,6 +1,6 @@ --- name: errortools -description: Use when writing or refactoring Rust error-handling code — covers idiomatic source-chain design with `thiserror` and ad-hock error logging. +description: Use when writing or refactoring Rust error-handling code — covers idiomatic source-chain design with `thiserror` and ad-hoc error logging. --- # Rust error-handling skill