dtolnay / thiserror

derive(Error) for struct and enum error types

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Can thiserror be converted into from arbitrary errors?

detly opened this issue · comments

I'm trying to create some structured error types for Calloop. The tricky thing here is that it's an event loop, so it not only has to pass out sensible errors, but handle errors from user code in callbacks and trait implementations (which may also then get passed out as well).

I started off with something like this:

#[derive(thiserror::Error, Debug)]
enum Error {
    #[error("invalid token provided to internal function")]
    InvalidToken,

    #[error("underlying IO error")]
    IoError(#[from] std::io::Error),

    #[error("error generated by event source or callback")]
    CallbackError(#[from] Box<dyn std::error::Error + Sync + Send>),
}

type Result<T> = core::result::Result<T, Error>;

The actual API looks like this:

fn process_events<F>(
    &mut self,
    readiness: Readiness,
    token: Token,
    callback: F
) -> Result<PostAction> where
    F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret, 

It is intended that users create their own implementations of this by composing lower level event sources, connecting these implementations together with callbacks if necessary. If those implementations use the error kinds of lower level systems (eg. std::io, 3rd party crates), then it very quickly becomes a burden to shoehorn all the different results into one return type (said from experience!).

(The callbacks, F, are called from process_events() but they're a bit of a red herring. They're not the hard part, because users can just make the associated type Ret be Result<_, calloop::Error> or whatever.)

Leaving aside Calloop's API for a moment, here's some minimal examples of what I mean (note the Result type is the alias above, not core::Result):

fn test_function_1() -> Result<String> {
    let v = &[0xDD1E, 0x006d];
    Ok(String::from_utf16(v)?)
}

fn test_function_2() -> Result<()> {
    Err(anyhow::anyhow!("oh no"))?;
    Ok(())
}

These are just examples. They could be any kind of errors, but they demonstrate my difficulty in accommodating arbitrary errors via thiserror. Unfortunately this fails to compile:

   Compiling playground v0.0.1 (/playground)
error[E0277]: `?` couldn't convert the error to `Error`
  --> src/lib.rs:17:29
   |
15 | fn test_function_1() -> Result<String> {
   |                         -------------- expected `Error` because of this
16 |     let v = &[0xDD1E, 0x006d];
17 |     Ok(String::from_utf16(v)?)
   |                             ^ the trait `From<FromUtf16Error>` is not implemented for `Error`
   |
   = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
   = help: the following implementations were found:
             <Error as From<Box<(dyn std::error::Error + Send + Sync + 'static)>>>
             <Error as From<std::io::Error>>
   = note: required because of the requirements on the impl of `FromResidual<std::result::Result<Infallible, FromUtf16Error>>` for `std::result::Result<String, Error>`
   = note: required by `from_residual`

error[E0277]: `?` couldn't convert the error to `Error`
  --> src/lib.rs:21:34
   |
20 | fn test_function_2() -> Result<()> {
   |                         ---------- expected `Error` because of this
21 |     Err(anyhow::anyhow!("oh no"))?;
   |                                  ^ the trait `From<anyhow::Error>` is not implemented for `Error`
   |
   = note: the question mark operation (`?`) implicitly performs a conversion   on the error value using the `From` trait
   = help: the following implementations were found:
             <Error as From<Box<(dyn std::error::Error + Send + Sync + 'static)>>>
             <Error as From<std::io::Error>>
   = note: required because of the requirements on the impl of `FromResidual<std::result::Result<Infallible, anyhow::Error>>` for `std::result::Result<(), Error>`
   = note: required by `from_residual`

However, it does compile if I add map_err() eg.

Ok(String::from_utf16(v).map_err(|e| Error::CallbackError(e.into()))?)

But this is exactly the kind of thing I'm trying to make more ergonomic by implementing a proper error type! If users are going to have to sprinkle map_err() everywhere, there's no gain from codifying error types. The really annoying thing is, it's all identical boilerplate, which seems like it should be covered by some kind of conversion. The exact same snippet, map_err(|e| Error::CallbackError(e.into())), works on FromUtf16Error, anyhow::Error, etc. etc. All it does is call into()!

I had thought that I could implement a trait to do this for me eg.

impl<E: std::error::Error> From<E> for Error {
    fn from(error: E) -> Self {
        Self::CallbackError(error.into())
    }
}

Nope!

   Compiling playground v0.0.1 (/playground)
error[E0119]: conflicting implementations of trait `std::convert::From<Error>` for type `Error`
  --> src/lib.rs:13:1
   |
13 | impl<E: std::error::Error> From<E> for Error {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: conflicting implementation in crate `core`:
           - impl<T> From<T> for T;

(As an aside, this is extremely baffling to me — I can't implement a conversion from the trait my error type implements to my own error type... because there's already a conversion defined in core. But the conversion in core doesn't actually handle the conversion that I'm trying to define! Huh?)

At this point, I'm 100% stumped. Is there any way to use thiserror to have a custom error type that works automatically with ? without needing map_err()s everywhere that do nothing but call into()? Or to implement From for std::error::Error for something that, itself, implements std::error::Error?

Perhaps this has nothing to do with thiserror, and there's simply no way to do it in the language itself. But thiserror's docs hint at having transparent and adaptable error types, so I thought I'd at least ask before giving up on it.

This seems like it would require specialization. Otherwise, I guess you could create a macro to contain the repeated code? Also, using Box<dyn ...> possibly with downcast could work? (Does implementing From<dyn ...> or similar work?)

Otherwise, I guess you could create a macro to contain the repeated code?

The repeated code is mostly just .map_err(Into::into), I'm not sure I could make a macro more concise than that. It's really just not needing a caller to have to worry about adding things before ? at all, but it sounds like that's not possible.

Does implementing From<dyn ...> or similar work?

Isn't that what:

#[error("error generated by event source or callback")]
CallbackError(#[from] Box<dyn std::error::Error + Sync + Send>)

...does? Or am I mistaken?

Having same issue - there must be something I'm not understanding in Rust because anyhow does this, but perhaps it takes some wizardry I'm not aware of. At the moment, I'm using anyhow as my result type and effectively wrapping my own error type with it, but I'd sure like to implement this same behavior in my own error type.

@nu11ptr Someone on the user forum pointed out that anyhow actually doesn't implement Error to get around this.

The thing I can't understand is that there's a blanket impl for converting Errors in core: std::convert::From<Error> for type Error. So why can't I use it to convert errors?

If this is still an issue, please take this question to any of the resources shown in https://www.rust-lang.org/community.

Just to clarify some things for anyone who comes across this with similar grief:

As an aside, this is extremely baffling to me — I can't implement a conversion from the trait my error type implements to my own error type... because there's already a conversion defined in core. But the conversion in core doesn't actually handle the conversion that I'm trying to define! Huh?

I have since learnt that the point here is that core, as the "owner" of both the Error trait and the From trait, might one day want to change how its blanket impl works on these two, and so the orphan rule kind of "reserves the right" for it to do that.

The thing I can't understand is that there's a blanket impl for converting Errors in core: std::convert::From for type Error. So why can't I use it to convert errors?

Using ? is not like, say, deref coercion where the compiler just adds as many as it needs to make a thing compile. In all of my examples, I need a chain of two conversions to get from the function's error to my custom error, that is:

(function's error) -> Box<dyn Error ...> -> (custom error)

The ? operator will only automate one level of this.

I'm trying to create some structured error types for Calloop.

The way I ended up doing this was (a) make a new associated type for event sources with the trait bound Into<Box<dyn Error + Sync + Send>> (note the Into!) and adjusting internal event loop code to accomodate this. It has turned out to be quite a useful interface and does not prevent implementors from using fully structured errors or eg. anyhow.