Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
mhnap opened this issue Aug 29, 2024 · 1 comment
Closed

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

mhnap opened this issue Aug 29, 2024 · 1 comment

Comments

@mhnap
Copy link

mhnap commented Aug 29, 2024

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.

@dtolnay
Copy link
Owner

dtolnay commented Nov 5, 2024

I've merged this issue into #103. There is no relevant distinction between structs vs enums for this purpose. The feature request is for #[error(transparent)] on fields.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants