How to create an Error impl that can be converted to without map_err boilerplate

I'm trying to create some structured error types for Calloop. It's an event loop, so it not only has to handle internal errors, but user provided callbacks and user implementations of its traits. There's a lot of overlap with IO errors, so here's where I'm starting from:

#[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>;

I don't particularly care about using thiserror, it's helpful but it's not mandatory.

Here's the sort of usage I'm aiming for (note the Result type is an alias, 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. 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! 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;

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!

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

The compiler tells you exactly what traits to implement: From<FromUtf16Error> and From<anyhow::Error>. You can't write a blanket impl to cover all cases generically, but you can cover all the cases you actually need by impl'ing From for the error types you need to convert from.

These are examples though. I have no idea what errors might arise in user code. I would like users to be able to use the library without having to constantly convert results and errors everywhere they're handled.

Note that anyhow::Error doesn't actually implement Error precisely for this reason. You could possibly try opting for a similar solution. Alternatively, might it be possible to arrange things so that the user-defined callbacks always return a Result<T, Box<dyn std::error::Error + Sync + Send>>, which is then converted to your error type in your code where I assume the callback is actually called?

I'm not understanding it correctly, then. In what context are your "usage" examples? Is the user supposed to write test_function_1 or are you?

If it's the former, then you can just shift the responsibility to the user code and make them impl From<UsersCustomErrorType> for your_library::Error.

The user will typically do one or both of:

  1. Implement a trait from the library. The trait methods return our Result. If the user is implementing such a function then them implementing From our error type won't do anything for them.
  2. Pass a callback to the library API. This is constrained more or less in the same way as 1.

All I really want is to be able to let the user use ? without fussing around with conversions.

I had completely missed this detail! Maybe that's the way to go then, although it seems a bit awkward.

Having user functions return Result<T, Box<etc etc>> might work for callbacks, but is a little ugly for traits. I wonder if associated types could solve this though, although the trait in question already has three associated types (callback event type, callback mutable state type, callback return type).

How so? I think you misunderstood my suggestion. I was suggesting that they impl From<TheirOwnError> for your_library::Error, not the other way around. This impl will cause users' code to work with ? when the enclosing function returns your_library::Result.

Oh, I see what you're saying. Yes, that'll help somewhat. Still extra work for them, but at least it can be written once. Most of my own code that uses Calloop doesn't use a purpose made error type though, it's either handled or the underlying library's error type is passed out with ? + map_err(), which is what I'm trying to alleviate. It's just a shame there's no easy way to bridge the fairly trivial gap between "their code produces something that can be turned into Box<dyn Error>" and "my error type can convert from a Box<dyn Error>".

Practically the other way around is more flexible.

fn with_cb<F, E>(&self, cb: F) -> Result<Foo, E>
where
    F: FnOnce() -> Result<Bar, E>,
    E: From<LibError>,
{...}

Key point is that the method returns the user-defined error. If the user choose the anyhow-like error type they don't need to do anything more. If they choose the thiserror- like approach it's a matter of adding single TheLib(#[from] LibError), line.

1 Like

FYI, 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 not hard to tell the user how to do the right thing with the associated type Ret for the callback. The more difficult problem is finding a good return type for process_events() itself. It is intended that users create their own implementations of this by composing lower level event sources.

If those implementations use the error kinds of lower level systems, then it very quickly becomes a burden to shoehorn all the different results into one return type (said from experience!).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.