thiserror icon indicating copy to clipboard operation
thiserror copied to clipboard

Allow `#[error(transparent)]` on enum fields

Open mhnap opened this issue 1 year ago • 0 comments

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.

mhnap avatar Aug 29 '24 10:08 mhnap