# error-path
`error-path` is a lightweight **operational error address** layer for existing Rust error systems. It does not replace `anyhow`, application-specific types built with `thiserror`, `eyre`, or `error-stack`. Instead, it gives an error a stable address describing where it belongs in your system.
```text
login.api.request_login
```
Use that address consistently in logs, support workflows, operational documentation, UI messages, and an error catalog. This crate does not reconstruct a call history like a stack trace; it accumulates deliberate, stable operational names.
## Installation
For the attribute macros, enable the `macros` feature:
```toml
[dependencies]
error-path = { version = "0.1", features = ["macros"] }
```
For `ErrorPath` and `WithErrorPath` alone, no feature is required:
```toml
[dependencies]
error-path = "0.1"
```
The minimum supported Rust version is 1.77.
## Quick start
Choose a stable path segment at the innermost boundary, then add address segments at higher boundaries.
```rust
use error_path::{error_path, ErrorPath, ErrorPathExt, WithErrorPath};
#[error_path("login.api.request_login")]
fn request_login() -> Result<(), ServiceError> {
Err(ServiceError::new())
}
#[derive(Debug)]
struct ServiceError {
path: ErrorPath,
}
impl ServiceError {
fn new() -> Self {
Self { path: ErrorPath::new() }
}
}
impl WithErrorPath for ServiceError {
fn with_error_path(mut self, path: &'static str) -> Self {
self.path.prepend_path(path);
self
}
}
impl ErrorPathExt for ServiceError {
fn error_path(&self) -> Option<ErrorPath> {
Some(self.path.clone())
}
}
let error = request_login().unwrap_err();
assert_eq!(error.path.to_string(), "login.api.request_login");
```
The central contract is `WithErrorPath`: keep an `ErrorPath` in your own error type and implement the trait to use the same macros.
```rust
use error_path::{ErrorPath, WithErrorPath};
struct MyError { path: ErrorPath }
impl WithErrorPath for MyError {
fn with_error_path(mut self, path: &'static str) -> Self {
self.path.prepend_path(path);
self
}
}
```
## Read an address at an error-handling boundary
`WithErrorPath` accumulates an address; `ErrorPathExt` reads only the address from an error that already exists. Import the trait at a logging, UI, or support boundary, then call `error.error_path()`.
```rust
use error_path::ErrorPathExt;
let error = request_login().unwrap_err();
let path = error.error_path().expect("ServiceError stores ErrorPath");
assert_eq!(path.to_string(), "login.api.request_login");
```
The default result is `None` for an error type that does not implement `ErrorPathExt`.
## Addresses and paths
An address is ordered from outermost to innermost. `#[error_path]` and `#[error_path_impl]` prepend stable path segments.
Your error type can expose a structured `ErrorPath`. Use `segments()` directly in logs or JSON, without parsing a rendered string, and use `to_string_with()` to render it with a chosen delimiter.
## Attribute macros
The `macros` feature re-exports three attributes:
```rust
use error_path::{error_path_impl, error_path_skip};
struct LoginApi;
#[error_path_impl("login.api")]
impl LoginApi {
fn request_login(&self) -> Result<(), ServiceError> {
Err(ServiceError::new())
}
#[error_path_skip]
fn health_check(&self) -> Result<(), ServiceError> {
Ok(())
}
}
```
- `#[error_path]` uses the function name when no argument is given, or uses its string argument.
- `#[error_path_impl]` adds `{base}.{method_name}` to each result-returning method. Without an argument, the base is the trait name for a trait implementation or the type name for an inherent implementation.
- `#[error_path_skip]` excludes one method inside an `#[error_path_impl]` block.
Generated code calls `WithErrorPath::with_error_path(e, path)` on the returned `Err(E)`. The error type `E` must therefore implement `WithErrorPath`.
An address is prepended only when an annotated function returns `Err`. In a nested `A -> B -> C` call, a failure from `C` crosses return boundaries during unwind and becomes `A.B.C`. Function entry does not automatically receive a parent address; task-local, thread-local, and function-argument propagation are out of scope.
For `::`, configure the delimiter before the first `ErrorPath` creation or enrichment, then use the same delimiter in macro input.
```rust
use error_path::{error_path_impl, set_path_delimiter};
set_path_delimiter("::")?;
#[error_path_impl("login::api")]
impl LoginApi for LoginApiImpl {
fn request_login(&self) -> Result<(), ServiceError> { todo!() }
}
// login::api::request_login
```
## Optional features and adapters
Enable only the adapter you use:
```toml
[dependencies]
error-path = { version = "0.1", features = ["macros", "anyhow"] }
anyhow = "1"
```
- `macros`: re-export `#[error_path]`, `#[error_path_impl]`, and `#[error_path_skip]`
- `anyhow`: add and query macro paths as typed context
- `eyre`: add a path as `eyre::Report` context
- `error-stack`: attach `ErrorPathSegment` to `error_stack::Report<C>`
- `stacked-errors`: add a locationless kind to `stacked_errors::Error`
- `full`: enable every feature above
External adapters use each library's own context or attachment mechanism. `anyhow::Error`, `eyre::Report`, `error_stack::Report<C>`, and `stacked_errors::Error` all implement `ErrorPathExt`, so after macro boundaries `error.error_path()` returns the address segments added by macros. It never interprets or includes an original error message.
```rust
use error_path::{ErrorPathExt, WithErrorPath};
let error = anyhow::anyhow!("HTTP communication failed");
let error = error.with_error_path("request_login");
let error = error.with_error_path("login.api");
assert_eq!(error.error_path().unwrap().to_string(), "login.api.request_login");
```
An application-owned error can store an `ErrorPath` and implement `ErrorPathExt` for additional structured data. `thiserror` is a derive macro that creates application-specific error types, so it needs no adapter. Implement `WithErrorPath` on the generated type. A macro such as `anyhow::bail!` works when its output error type implements `WithErrorPath` or its adapter feature is enabled.
## Delimiter configuration
The default delimiter is `.`. At application startup, you may set one process-wide delimiter for parsing and display.
```rust
use error_path::set_path_delimiter;
set_path_delimiter("::").expect("configure it once before the first ErrorPath");
```
After configuration, `ErrorPath` parsing and formatting through `Display` and `to_string()` use `::`. Write macro arguments and direct prefixes with the same delimiter, for example `login::api::request_login`. If configuration has not happened before the first `ErrorPath` creation, enrichment, or formatting, the default `.` is fixed. An empty delimiter and a second configuration attempt return an error; no environment variable is read.
For a one-off format independent of global configuration, use `ErrorPath::to_string_with("/")`.
## Scope and limitations
- The crate works at `Result<T, E>` boundaries. It does not automatically capture or transform panics, threads, or errors inside spawned tasks.
- Paths are never inferred from runtime stack traces or source file paths. Choose stable operational names explicitly, or use macro defaults.
- Apply `#[error_path]` only to functions returning `Result`. `#[error_path_impl]` automatically wraps only methods whose return-type name ends in `Result`.
- Generated macros refer to `::error_path`; if you rename the dependency, re-export it under the `error_path` name.
- The global delimiter affects `ErrorPath` rendering. It does not rewrite context or attachments already stored by external adapters.
## Learn more
- [Project documentation and examples](https://github.com/yongaru/error-path-kit)
- [API documentation](https://docs.rs/error-path)
- [Changelog](https://github.com/yongaru/error-path-kit/blob/main/CHANGELOG.md)
## License
Licensed under either Apache-2.0 or MIT, at your option.
## Author
Created and maintained by 용사장. [akira76@gmail.com](mailto:akira76@gmail.com)