Recommendation on mixing exit codes with anyhow
- Dominant language
- Rust
- Stars
- 6.7k
- Forks
- 224
- PR merge metrics
- No merged PRs in 30d
Description
With Rust 1.61.0 out, the [`Termination`](https://doc.rust-lang.org/stable/std/process/trait.Termination.html) trait and associated [`ExitCode`](https://doc.rust-lang.org/stable/std/process/struct.ExitCode.html) are now stable. In the past, I maintained a large error enum purely to decipher what error code to return, printed out the error using `eprintln!`, and then exited with `std::process::exit`.
Now, I'm thinking through a way for me to switch over to anyhow because it would streamline so much in my application, but I still need some way to keep track of what exit code to return for different errors. Looking for any advice/thoughts on how I could capture/encode that information when bubbling up errors.
### Implement a wrapper type for `anyhow::Error` at the end
One way would be to create a newtype wrapper for the error and then use downcasting to figure out the underlying error with an associated exit code.
```rust
use std::process::{ExitCode, Termination};
struct AppResult(anyhow::Result<()>);
impl Termination for AppResult {
fn report(self) -> ExitCode {
match self {
Ok(_) => ExitCode::SUCCESS,
Err(x) => {
if self.downcast_ref::().is_some() {
ExitCode::from(11)
} else if self.downcast_ref::().is_some() {
ExitCode::from(22)
} else {
ExitCode::FAILURE
}
}
}
}
}
fn main() -> AppResult {
AppResult(real_main())
}
fn real_main() -> anyhow::Result<()> {
// ...
}
```
### Derive an error type with an exit code
```rust
use std::process::{ExitCode, Termination};
// Assume that this type implements:
// 1. Display that yields the context
// 2. std::error::Error
struct ExitCodeError {
error: Box,
exit_code: ExitCode,
}
struct AppResult(anyhow::Result<()>);
impl Termination for AppResult {
fn report(self) -> ExitCode {
match self {
Ok(_) => ExitCode::SUCCESS,
Err(x) => {
if let Some(x) = self.downcast::() {
x.exit_code
} else {
ExitCode::FAILURE
}
}
}
}
}
fn main() -> AppResult {
AppResult(real_main())
}
fn real_main() -> anyhow::Result<()> {
// ...
// For each error, we have to wrap the error in our ExitCodeError first if we want a unique exit code
// This seems really verbose still, so maybe there's a way to simplify
let value = do_something().map_err(|error| ExitCodeError {
error: Box::new(error),
exit_code: ExitCode::from(22)
})?;
// ...
}
```
### Some cleaner way?
Ideally, I'd love something like
```rust
use std::process::ExitCode;
fn main() -> anyhow::Result<()> {
std::fs::read("some/path")
.exit_context(ExitCode::from(22), "Failed with specific context")?;
// ...
}
```
Contributor guide
No contributing guide indexed for this repository
Research direction
No repository file or test is named. Start by reviewing the shown main/real_main flow alongside Rust's Termination and ExitCode APIs and anyhow's error propagation; done would require a decided, maintainable recommendation for associating errors with exit codes, rather than another open-ended design discussion.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100