Allow `#[error(transparent)]` on enum fields
Summary
I would like to propose extending the usage of #[error(transparent)] (or maybe adding a new #[transparent] attribute) for handling cases when there are other fields in the enum variant (similar to #103).
Motivation
I'm using thiserror for defining enum-like errors and each of them can have many variants, but my case is similar to:
#[derive(Debug, thiserror::Error)]
pub enum MyError {
#[error(transparent)]
Opaque(#[from] Box<dyn std::error::Error>),
}
Sometime later now I want to modify this one variant to support a backtrace, so I tried this:
#[error(transparent)]
Opaque(
#[from] Box<dyn std::error::Error>,
std::backtrace::Backtrace,
),
But this is not working because of error: #[error(transparent)] requires exactly one field.
Workaround 1
I can work around it by using a custom message in the Display impl:
#[error("My opaque error")]
Opaque(
#[from] Box<dyn std::error::Error>,
std::backtrace::Backtrace,
),
But this is also not working cause of error[E0658]: use of unstable library feature 'error_generic_member_access'.
I discovered #314 explaining that this is expected that the usage of Backtrace requires nightly. But I got curious why it generated unstable nightly code on stable in the first place. After some digging, I found #223 where is explained that this is done to be future-compatible if I understand correctly.
So, I tried to remove the Backtrace type by using a type alias:
type MyBt = std::backtrace::Backtrace;
#[derive(Debug, thiserror::Error)]
pub enum MyError {
#[error("My opaque error")]
Opaque(#[from] Box<dyn std::error::Error>, MyBt),
}
This indeed helped to not generate nightly code, but now it fails with error: deriving From requires no fields other than source and backtrace.
This is not a problem cause I still planned to capture the backtrace in the From impl, so finally now I have a fully working version:
type MyBt = std::backtrace::Backtrace;
#[derive(Debug, thiserror::Error)]
pub enum MyError {
#[error("My opaque error")]
Opaque(#[source] Box<dyn std::error::Error>, MyBt),
}
impl From<Box<dyn std::error::Error>> for MyError {
fn from(value: Box<dyn std::error::Error>) -> Self {
Self::Opaque(value.into(), MyBt::capture())
}
}
Drawbacks
The main drawback is that I need to specify some error message in #[error()]. This can be not a problem (e.g. IO error), but in my case of opaque error, it should indeed be strictly transparent.
Workaround 2
Another workaround would be to leave only one field with #[error(transparent)] but to change its type to include a backtrace inside it:
#[derive(Debug)]
struct Opaque {
error: Box<dyn std::error::Error>,
backtrace: std::backtrace::Backtrace,
}
impl std::fmt::Display for Opaque {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.error, f)
}
}
impl std::ops::Deref for Opaque {
type Target = dyn std::error::Error;
fn deref(&self) -> &Self::Target {
self.error.as_ref()
}
}
impl<E> From<E> for Opaque
where
E: std::error::Error + 'static,
{
fn from(value: E) -> Self {
Self {
error: value.into(),
backtrace: std::backtrace::Backtrace::capture(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum MyError {
#[error(transparent)]
Opaque(#[from] Opaque),
}
Drawbacks
This indeed works as expected by now I need to maintain an additional Opaque type.
Workaround 3
I could use anyhow::Error instead of Box<dyn std::error::Error> and backtrace, but in this case, I cannot use anyhow::Error as for certain reasons I need standard Debug impl (to print backtraces from nested errors, as I bumped also into #174).
Workaround 4
I can switch from thiserror to manual traits implementation, but some of the enum-like errors are huge and it would require a considerable amount of time to change.
I think this also can be usable with Location (#142) or any other field as well, so something like this would be possible:
#[derive(Debug, thiserror::Error)]
pub enum MyError {
#[error(transparent)]
Opaque(
#[transparent] Box<dyn std::error::Error>,
std::panic::Location<'static>,
),
}
impl From<Box<dyn std::error::Error>> for MyError {
#[track_caller]
fn from(value: Box<dyn std::error::Error>) -> Self {
Self::Opaque(value.into(), *std::panic::Location::caller())
}
}
Thanks for such a great crate.