# charon-error
[](https://crates.io/crates/charon-error)
[](https://docs.rs/charon-error/latest/charon_error/)
[](LICENSE-APACHE)
[](LICENSE-MIT)
Structured error handling for Rust with rich error reports, data sensitivity labels, panic capture, and GitLab issue integration.
Named after **Charon**, the Greek ferryman of the dead across the river Styx. This crate ferries your program from a running state to a graceful death, collecting every detail along the way.
## Features
- **Error chains** — Stack multiple errors with `ErrorReport` to preserve the full context from root cause to top-level failure
- **Panic hooks** — Capture panics and display human-readable error messages with `setup_panic!` and `setup_panic_simple!`
- **Issue submission** — Generate pre-filled GitLab issue URLs directly from errors so users can report bugs with few clicks
- **Sensitivity labels** — Tag data as `Public`, `Private`, `Internal`, or `Confidential` to control what information gets shared in reports
- **Tracing integration** — Works with the `tracing` crate. Use `#[instrument]` and span traces are captured automatically in error frames
- **Flexible formatting** — Configure output detail level (Compact, Medium, Full, Debug) and indentation style
## Example Output
```
PANIC: A panic occurred during execution.
* Message: 'Error: `Called `ResultExt::unwrap_error()` on an `Err` value`'
* Path: 'src/main.rs:58:21'
PANIC: The application encountered an error it could not recover from.
{
last_error: 'Error: `Called `ResultExt::unwrap_error()` on an `Err` value`',
unique_id: 'IpRUIEGnbEh1r58c14jc',
frames: [
{
message: 'No such file or directory (os error 2)',
source_location: src/cli/config.rs:15:10,
span_trace: [
charon_demo::cli::config::read_config at (src/cli/config.rs:7:0),
charon_demo::cli::start_server at (src/cli.rs:82:0),
charon_demo::cli::start at (src/cli.rs:35:0),
],
attachments: {},
date_time: 2026-02-27T21:56:38.787453133+00:00,
},
{
message: 'The config file failed to load correctly.',
source_location: src/cli/config.rs:15:10,
span_trace: [
charon_demo::cli::config::read_config at (src/cli/config.rs:7:0),
charon_demo::cli::start_server at (src/cli.rs:82:0),
charon_demo::cli::start at (src/cli.rs:35:0),
],
attachments: {
file_path: '../config.toml',
},
date_time: 2026-02-27T21:56:38.787580742+00:00,
},
{
message: 'Error: `Called `ResultExt::unwrap_error()` on an `Err` value`',
source_location: src/main.rs:58:21,
span_trace: [],
attachments: {},
date_time: 2026-02-27T21:56:38.787617912+00:00,
},
],
}
┌──────────────────────────┐
│ Open/Submit Bug Report │
└──────────────────────────┘
```
NOTE: For `span_trace` to be filled in, you need to register a [tracing::Subscriber](https://docs.rs/tracing/latest/tracing/trait.Subscriber.html)
This can be done though the [`tracing-subscriber`](https://crates.io/crates/tracing-subscriber) crate or by using [`charon-logging`](https://crates.io/crates/charon-logging).
<details>
<summary><b>View more Error Reports and Screenshots</b></summary>
~~~
PANIC: A panic occurred during execution.
* Message: 'Error: `Called `ResultExt::unwrap_error()` on an `Err` value`'
* Path: 'src/main.rs:58:21'
PANIC: The application encountered an error it could not recover from.
If you report this we might be able to fix this in the future.
Error: `Called `ResultExt::unwrap_error()` on an `Err` value`
Panic Info:
## Summary:
```hcl
{
last_error: 'Error: `Called `ResultExt::unwrap_error()` on an `Err` value`',
unique_id: 'zyzIoan0VCBur1AxeQZx',
frames: [
{
message: 'No such file or directory (os error 2)',
source_location: src/cli/config.rs:15:10,
span_trace: [
charon_demo::cli::config::read_config at (src/cli/config.rs:7:0),
charon_demo::cli::start_server at (src/cli.rs:82:0),
charon_demo::cli::start at (src/cli.rs:35:0),
],
attachments: {},
},
{
message: 'The config file failed to load correctly.',
source_location: src/cli/config.rs:15:10,
span_trace: [
charon_demo::cli::config::read_config at (src/cli/config.rs:7:0),
charon_demo::cli::start_server at (src/cli.rs:82:0),
charon_demo::cli::start at (src/cli.rs:35:0),
],
attachments: {
file_path: '../config.toml',
},
},
{
message: 'Error: `Called `ResultExt::unwrap_error()` on an `Err` value`',
source_location: src/main.rs:58:21,
span_trace: [],
attachments: {},
},
],
}
```
## Debug info:
### Backtrace:
```
1: ErrorFrame::new
2: ErrorReport::push_error
3: charon_demo::main
4: backtrace::__rust_begin_short_backtrace
5: lang_start::{{closure}}
6: rt::lang_start_internal
7: main
8: __libc_start_call_main => ./csu/../sysdeps/nptl/libc_start_call_main.h:58
9: __libc_start_main_impl => ./csu/../csu/libc-start.c:360
10: _start
```
### System:
* Application version: 0.3.0
* System: linux x86_64
Please report this error. Issue title: `Error: `Called `ResultExt::unwrap_error()` on an `Err` value``
- Step 1: Check if error report already exists: https://gitlab.com/%3Cproject_path%3E/%3Cproject_name%3E/-/issues/?sort=created_date&state=all&in=TITLE&search=Error%3A+%60Called+%60ResultExt%3A%3Aunwrap_error%28%29%60+on+an+%60Err%60+value%60
- Step 2: Report issue (if it does not already exists):
- Open Pre-created Report Link:
---
https://gitlab.com/%3Cproject_path%3E/%3Cproject_name%3E/issues/new?...
```
~~~


</details>
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
charon-error = "0.3"
```
## Quick Start
```rust
use charon_error::prelude::*;
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("failed to load configuration")]
ConfigLoad,
#[error("failed to start server")]
ServerStart,
}
#[instrument]
fn load_config() -> ResultER<String> {
std::fs::read_to_string("config.toml")
.change_context(AppError::ConfigLoad)
}
#[instrument]
fn start_server() -> ResultER<()> {
let _config = load_config()
.change_context(AppError::ServerStart)?;
Ok(())
}
```
`ResultER<T>` is a type alias for `Result<T, ErrorReport>`. The `ResultExt` trait (imported via the prelude)
adds `.change_context()`, `.error_attach()`, and `.unwrap_error()` to any `Result` or `Option`.
## Error Context and Attachments
### Adding Context
Use `.change_context()` to wrap errors with higher-level meaning:
```rust
use charon_error::prelude::*;
#[instrument]
fn read_user(id: u64) -> ResultER<String> {
std::fs::read_to_string(format!("users/{id}.json"))
.change_context(StringError::new("failed to read user"))
}
```
### Attaching Data
Attach diagnostic data to errors with sensitivity labels:
```rust
use charon_error::prelude::*;
#[instrument]
fn process_order(order_id: &str) -> ResultER<()> {
do_work()
.error_attach_public_string("order_id", order_id.to_owned())?;
Ok(())
}
fn do_work() -> ResultER<()> { Ok(()) }
```
## Sensitivity Labels
`ErrorSensitivityLabel` controls who can see attached data:
| `Public` | yes | yes | yes | no |
| `Private` | yes | no | yes | no |
| `PrivateConfidential`| yes | no | no | yes |
| `Internal` | no | no | yes | no |
| `Confidential` | no | no | yes | yes |
| `HighlyConfidential` | no | no | yes | yes |
Note: Encryption is not implemented yet.
## Panic Hooks
### Full Panic Hook with Issue Submission
```rust
use charon_error::prelude::*;
use charon_error::prelude::gitlab_er::*;
#[allow(dead_code)]
fn check_if_common_errors(error_message: &str) -> Option<String> {
// use colored::*;
match error_message {
"Address/Port binding error." => {
Some("We got a Address/Port binding error.\n\
You most likely already have the application already open somewhere. \
Because the port is already in use. If this is not the case an other app \
is using this port. You can change the port in the config file.\n\
Change the port `config.json` -> server -> port.\n\
Change the value to 20001 and try again.".to_owned())
},
_ => {
None
},
}
}
fn main() -> ResultER<()> {
// Configure global error report settings
ERGlobalSettings::set_global_settings(ERGlobalSettings {
check_known_error_types_fn: check_if_common_errors,
submit_error_reporter_fn: |er| Box::new(GitLabErrorReport::new(er)),
..Default::default()
})
.unwrap_error();
// Configure GitLab integration
GitLabERGlobalSettings::set_global_settings(GitLabERGlobalSettings {
domain: "gitlab.com".to_owned(),
project_path: "my-group/my-project".to_owned(),
labels: vec!["bug".to_owned(), "auto-generated".to_owned()],
url_char_limit: 6000,
..Default::default()
})
.unwrap_error();
// Install panic hook
setup_panic!();
// Your application code here
Ok(())
}
```
When a panic occurs:
- **Known errors**: Displays a friendly message from `check_if_common_errors`
- **Unknown errors (debug build)**: Shows the full error report with a clickable link to create a GitLab issue
- **Unknown errors (release build)**: Shows a user-friendly message with report instructions
### Simple Panic Hook
For applications that do not need issue submission:
```rust
use charon_error::prelude::*;
fn main() {
setup_panic_simple!();
// Your application code here
}
```
This panic hook is mainly for when the more full featured panic hook is to much and just want to setup a simple hook.
This hook can also be used as a fallback panic hook.
Output is much simpler and faster, for example:
```
PANIC: The application encountered an error it could not recover from.
* Message: 'Error: `Called `ResultExt::unwrap_error()` on an `Err` value`'
* Path: 'src/main.rs:72:21'
```
## Tracing Integration
`charon-error` integrates with the `tracing` crate. Annotate functions with `#[instrument]` and span traces are automatically captured in each error frame:
```rust
use charon_error::prelude::*;
#[instrument]
fn process_request(user_id: u64) -> ResultER<()> {
validate_user(user_id)?;
Ok(())
}
#[instrument]
fn validate_user(id: u64) -> ResultER<()> {
Err(StringError::new("user not found"))?
}
```
The error report will include the span trace showing the call chain through `process_request` and `validate_user`.
## Roadmap
- [ ] GitHub issue submission support
- [ ] Encryption for `Confidential` and `HighlyConfidential` data
- [ ] Configurable global formatting settings
## Use of AI in this project
This project was hand written before AI. We will be using AI to assist in development.
However ALL code is manually reviewed and improve.
Example uses of AI: documentation, verification, testing, as double check, ...
---
## License
The code in this project is licensed under the MIT or Apache 2.0 license.
All contributions, code and documentation, to this project will be similarly licensed.