Idiomatic error propagation
- Dominant language
- Rust
- Stars
- 5.5k
- Forks
- 213
- PR merge metrics
- No merged PRs in 30d
Description
Currently, we have a number of crates where we have a top-level `Error` `enum`, but our functions return `anyhow::Error` instead of the `enum`. It's been pointed out that this is not idiomatic and it would be better to just return `Error` and use `thiserror`'s transparent forwarding. I'm confused about how to idiomatically handle matching on forwarded error variants. Consider the following crate structure:
```
high-level
/ \
low-level-a low-level-b
```
where `high-level` defines `Error`, `low-level-a` defines `ErrorA` and `low-level-b` defines `ErrorB`. If each of the `enum`s includes an `IoError(std::io::Error)` variant, how is code supposed to match on the `std::io::Error`? To make the question concrete, consider [this code](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=0626981d851e090bd9422cf1b6f18400), which is what I currently imagine:
```rust
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("something bad happened: {0}")]
Bad(String),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
A(ErrorA),
#[error(transparent)]
B(ErrorB),
}
#[derive(thiserror::Error, Debug)]
enum ErrorA {
#[error(transparent)]
IoError(#[from] std::io::Error),
}
#[derive(thiserror::Error, Debug)]
enum ErrorB {
#[error(transparent)]
IoError(#[from] std::io::Error),
}
fn f() -> Result<(), Error> {
Err(Error::Bad("ouch".into()))
}
fn main() {
match f() {
Ok(()) => {
}
Err(Error::IoError(err))
| Err(Error::A(ErrorA::IoError(err)))
| Err(Error::B(ErrorB::IoError(err))) => {
// Handle the io error.
eprintln!("io error: {}", err);
}
Err(err) => {
eprintln!("An error occured: {}", err);
}
}
}
```
[This](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=50e61864faf905fee75bba3634205caf) is how we currently do it:
```rust
#[derive(thiserror::Error, Debug)]
enum Error {
#[error("something bad happened: {0}")]
Bad(String),
}
#[derive(thiserror::Error, Debug)]
enum ErrorA {
}
#[derive(thiserror::Error, Debug)]
enum ErrorB {
}
fn f() -> anyhow::Result<()> {
Err(std::io::Error::new(std::io::ErrorKind::Other, "oh no!").into())
}
fn main() {
let result = f();
match result {
Err(err) => {
if let Some(err) = err.downcast_ref::() {
// Handle the io error.
eprintln!("io error: {}", err);
} else {
eprintln!("Not an io error: {}", err);
}
}
Ok(()) => {
eprintln!("Everything is fine")
}
}
}
```
That is, we downcast to `std::io::Error` and it doesn't matter if the `std::io::Error` comes from `high-level`, `low-level-a`, `low-level-b`, or another crate that `high-level` starts using later: the user of the high-level API can reliably and compactly catch `std::io::Error`s.
Thanks for any insights!
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.