Skip to content

Commit 1c518f2

Browse files
authored
docs: improve docs all over the place
* add detailed docs on the macros * much improved library documentation * add better intro example * improve readme
2 parents a00ff9b + 334a696 commit 1c518f2

File tree

7 files changed

+361
-172
lines changed

7 files changed

+361
-172
lines changed

Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22
name = "n0-error"
33
version = "0.1.0"
44
edition = "2024"
5+
readme = "README.md"
6+
description = "ergonomic errors with call-site location"
7+
license = "MIT OR Apache-2.0"
8+
authors = ["Frando <[email protected]>", "n0 team"]
9+
repository = "https://github.com/n0-computer/n0-error"
10+
keywords = ["error", "backtrace", "location"]
511

612
[dependencies]
713
n0-error-macros = { path = "n0-error-macros" }
@@ -17,5 +23,5 @@ inherits = 'dev'
1723
opt-level = 1
1824

1925
[features]
20-
default = ["anyhow"]
26+
default = []
2127
anyhow = ["dep:anyhow"]

README.md

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
# n0-error
22

3-
An error library that supports tracking the call-site location of errors.
3+
[![Documentation](https://docs.rs/n0-error/badge.svg)](https://docs.rs/n0-error)
4+
[![Crates.io](https://img.shields.io/crates/v/n0-error.svg)](https://crates.io/crates/n0-error)
45

5-
This crate provides a trait and proc macro to ergonomically work with enum or struct errors. The macro can add
6-
a `meta` field to structs and enum variants, which stores the call-site location of errors. The `StackError`
7-
trait provides access to this metadata for both the current error, and all its sources, as long as they also
8-
implement `StackError`.
6+
An Rust error library that supports tracking the call-site location of errors.
97

10-
Additionally, this crate provides an anyhow-style `AnyError` type, which is a type-erased container for either
11-
`StackError` or `std::error::Error` errors.
8+
This crate provides a `StackError` trait and proc macro to ergonomically work with nested
9+
enum and struct errors, while allowing to track the call-site location for the
10+
full error chain.
11+
12+
It also has a `AnyError` type that works similar to anyhow errors while keeping
13+
the location metadata of `StackError` errors accessible through the full error chain.
14+
15+
## Example
1216

1317
```rust
14-
use n0_error::{e, stack_error, StackError, Result, StackResultExt, StdResultExt};
18+
use n0_error::{e, ensure, Result, StackResultExt, stack_error};
1519

1620
/// The `stack_error` macro controls how to turn our enum into a `StackError`.
1721
///
@@ -21,9 +25,9 @@ use n0_error::{e, stack_error, StackError, Result, StackResultExt, StdResultExt}
2125
#[stack_error(derive, add_meta, from_sources)]
2226
enum MyError {
2327
/// We can define the error message with the `error` attribute
24-
#[error("bad input ({count})")]
25-
BadInput { count: usize },
26-
/// Or we can define a variant as `transparent`, which forwards the Display impl to the error source
28+
#[error("invalid input")]
29+
InvalidInput { source: InvalidInput },
30+
/// Or we can define a variant as `transparent`, which forwards the Display impl to the error source.
2731
#[error(transparent)]
2832
Io {
2933
/// For sources that do not implement `StackError`, we have to mark the source as `std_err`.
@@ -32,54 +36,85 @@ enum MyError {
3236
},
3337
}
3438

35-
// A function that returns a std::io::Error
36-
fn fail_io() -> std::io::Result<()> {
37-
Err(std::io::Error::other("io failed"))
39+
/// We can use the [`stack_error`] macro on structs as well.
40+
#[stack_error(derive, add_meta)]
41+
#[error("wanted {expected} but got {actual}")]
42+
struct InvalidInput {
43+
expected: u32,
44+
actual: u32,
3845
}
3946

40-
// An outer function returning our custom MyError
41-
fn some_fn(count: usize) -> Result<(), MyError> {
42-
if count == 13 {
43-
// The `e` macro constructs a `StackError` while automatically adding the `meta` field.
44-
return Err(e!(MyError::BadInput { count }));
47+
fn validate_input(input: u32) -> Result<(), InvalidInput> {
48+
if input != 23 {
49+
// With the `e` macro we can construct an error directly without spelling out the `meta` field:
50+
return Err(e!(InvalidInput { expected: 12, actual: input }))
4551
}
46-
// We have a `From` impl for `std::io::Error` on our error.
47-
fail_io()?;
48-
// Without the From impl, we'd need to forward the error manually.
49-
// The `e` macro can assist here, so that we don't have to declare the `meta` field manually.
50-
fail_io().map_err(|source| e!(MyError::Io, source))?;
52+
/// There's also `bail` and `ensure` macros that expand to include the `meta` field:
53+
n0_error::ensure!(input == 23, InvalidInput { expected: 23, actual: input });
5154
Ok(())
5255
}
5356

54-
// A main function that returns AnyError (via the crate's Result alias)
55-
fn run() -> Result<()> {
56-
// We can add context to errors via the result extensions.
57-
// The `context` function adds context to any `StackError`.
58-
some_fn(13).context("failed at some_fn")?;
59-
// To add context to std errors, we have to use `std_context` from `StdResultExt`.
60-
fail_io().std_context("failed at fail_io")?;
57+
/// The `Result` type defaults to `AnyError` for the error variant.
58+
///
59+
/// Errors types using the derive macro convert to `AnyError`, and we can add additional context
60+
/// with the result extensions.
61+
fn process(input: u32) -> Result<()> {
62+
validate_input(input).context("failed to process input")?;
6163
Ok(())
6264
}
65+
```
6366

64-
fn main() -> Result<()> {
65-
let err = run().unwrap_err();
66-
assert_eq!(
67-
format!("{err:#}"),
68-
"failed at some_fn: bad input (13)"
69-
);
70-
Ok(())
71-
}
67+
The error returned from `process` would look like this in the alternate display format:
68+
```text
69+
failed to process input: invalid input: wanted 23 but got 13
70+
```
71+
and like this in the debug format with `RUST_BACKTRACE=1` or `RUST_ERROR_LOCATION=1`:
72+
```text
73+
failed to process input (examples/basic.rs:61:17)
74+
Caused by:
75+
invalid input (examples/basic.rs:36:5)
76+
wanted 23 but got 13 (examples/basic.rs:48:13)
77+
```
7278

73-
/// You can also use the macros with tuple structs or enums.
74-
/// In this case the meta field will be added as the last field.
75-
#[stack_error(derive, add_meta)]
76-
#[error("tuple fail ({_0})")]
77-
struct TupleStruct(u32);
79+
### Details
7880

79-
#[stack_error(derive, add_meta)]
80-
enum TupleEnum {
81-
#[error("io failed")]
82-
Io(#[error(source, std_err)] std::io::Error),
83-
}
81+
- All errors using the macro implement the `StackError` trait, which exposes call-site metadata for
82+
where the error occurred. Its `source` method returns references which may be other stack errors,
83+
allowing access to location data for the entire error chain.
84+
- The proc macro can add a `meta` field to structs or enum variants. This field stores call-site
85+
metadata accessed through the `StackError` trait.
86+
* Call-site metadata in the `meta` field is collected only if `RUST_BACKTRACE=1` or
87+
`RUST_ERROR_LOCATION=1` env variable is set. Otherwise, it is disabled to avoid runtime overhead.
88+
* The declarative macro `e!` provides an ergonomic way to construct such errors without
89+
explicitly setting the field. The crate's `ensure` and `bail` macro also do this.
90+
- The crate includes an `AnyError` type, similar to `anyhow::Error`. When created from a
91+
`StackError`, the call-site metadata is preserved. `AnyError` is recommended for applications and
92+
tests, while libraries should use concrete derived errors.
93+
* All stack errors convert to `AnyError`, so they can be propagated to such results with `?`.
94+
* For std errors, use `std_context` or `anyerr` to convert to `AnyError`. For stack errors, use
95+
`context` or simply propagate with `?`.
96+
* Result extension traits provide conversions between results with `StackError`s,
97+
`std::error::Error`s to `AnyError`, with support for attaching context.
98+
- Both `AnyError` and all errors using the `StackError` derive feature consistent, structured output
99+
that includes location metadata when available.
84100

85-
```
101+
### Feature flags
102+
103+
* `anyhow` (off by default): Enables `From<anyhow::Error> for AnyError` and `From<AnyError> for anyhow::Error`
104+
105+
## License
106+
107+
Copyright 2025 N0, INC.
108+
109+
This project is licensed under either of
110+
111+
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
112+
http://www.apache.org/licenses/LICENSE-2.0)
113+
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
114+
http://opensource.org/licenses/MIT)
115+
116+
at your option.
117+
118+
## Contribution
119+
120+
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

examples/basic.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use n0_error::{Result, StackResultExt, StdResultExt, e, stack_error};
2+
3+
/// The `stack_error` macro controls how to turn our enum into a `StackError`.
4+
///
5+
/// * `add_meta` adds a field to all variants to track the call-site error location
6+
/// * `derive` adds `#[derive(StackError)]`
7+
/// * `from_sources` creates `From` impls for the error sources
8+
#[stack_error(derive, add_meta, from_sources)]
9+
enum MyError {
10+
/// We can define the error message with the `error` attribute
11+
/// It should not include the error source, those are printed in addition depending on the output format.
12+
#[error("invalid input")]
13+
InvalidInput { source: InvalidInput },
14+
/// Or we can define a variant as `transparent`, which forwards the Display impl to the error source
15+
#[error(transparent)]
16+
Io {
17+
/// For sources that do not implement `StackError`, we have to mark the source as `std_err`.
18+
#[error(std_err)]
19+
source: std::io::Error,
20+
},
21+
}
22+
23+
/// We can use the [`stack_error`] macro on structs as well.
24+
#[stack_error(derive, add_meta)]
25+
#[error("wanted {expected} but got {actual}")]
26+
struct InvalidInput {
27+
expected: u32,
28+
actual: u32,
29+
}
30+
31+
fn validate_input(number: u32) -> Result<(), InvalidInput> {
32+
if number != 23 {
33+
// The `e` macro constructs a `StackError` while automatically adding the `meta` field.
34+
Err(e!(InvalidInput {
35+
actual: number,
36+
expected: 23
37+
}))
38+
} else {
39+
Ok(())
40+
}
41+
}
42+
43+
fn fail_io() -> std::io::Result<()> {
44+
Err(std::io::Error::other("io failed"))
45+
}
46+
47+
/// Some function that returns [`MyError`].
48+
fn process(number: u32) -> Result<(), MyError> {
49+
// We have a `From` impl for `InvalidInput` on our error.
50+
validate_input(number)?;
51+
// We have a `From` impl for `std::io::Error` on our error.
52+
fail_io()?;
53+
// Without the From impl, we'd need to forward the error manually.
54+
// The `e` macro can assist here, so that we don't have to declare the `meta` field manually.
55+
fail_io().map_err(|source| e!(MyError::Io, source))?;
56+
Ok(())
57+
}
58+
59+
// A main function that returns AnyError (via the crate's Result alias)
60+
fn run(number: u32) -> Result<()> {
61+
// We can add context to errors via the result extensions.
62+
// The `context` function adds context to any `StackError`.
63+
process(number).context("failed to process input")?;
64+
// To add context to std errors, we have to use `std_context` from `StdResultExt`.
65+
fail_io().std_context("failed at fail_io")?;
66+
Ok(())
67+
}
68+
69+
fn main() -> Result<()> {
70+
if let Err(err) = run(13) {
71+
println!("{err}");
72+
// failed to process input
73+
74+
println!("{err:#}");
75+
// failed to process input: invalid input: wanted 23 but got 13
76+
77+
println!("{err:?}");
78+
// failed to process input
79+
// Caused by:
80+
// invalid input
81+
// wanted 23 but got 13
82+
83+
// and with RUST_BACKTRACE=1 or RUST_ERROR_LOCATION=1
84+
// failed to process input (examples/basic.rs:61:17)
85+
// Caused by:
86+
// invalid input (examples/basic.rs:36:5)
87+
// wanted 23 but got 13 (examples/basic.rs:48:13)
88+
89+
println!("{err:#?}");
90+
// Stack(WithSource {
91+
// message: "failed to process input",
92+
// source: Stack(InvalidInput {
93+
// source: BadNumber {
94+
// expected: 23,
95+
// actual: 13,
96+
// meta: Meta(examples/basic.rs:48:13),
97+
// },
98+
// meta: Meta(examples/basic.rs:36:5),
99+
// }),
100+
// meta: Meta(examples/basic.rs:61:17),
101+
// })
102+
}
103+
Ok(())
104+
}
105+
106+
/// You can also use the macros with tuple structs or enums.
107+
/// In this case the meta field will be added as the last field.
108+
#[stack_error(derive, add_meta)]
109+
#[error("tuple fail ({_0})")]
110+
struct TupleStruct(u32);
111+
112+
#[stack_error(derive, add_meta)]
113+
#[allow(unused)]
114+
enum TupleEnum {
115+
#[error("io failed")]
116+
Io(#[error(source, std_err)] std::io::Error),
117+
}

0 commit comments

Comments
 (0)