Bittrance
Bittrance

Reputation: 2260

How to match custom Fails with the failure crate

I'm trying to understand how to use the failure crate. It works splendidly as a unification of different types of standard errors, but when creating custom errors (Fails), I do not understand how to match for custom errors. For example:

use failure::{Fail, Error};

#[derive(Debug, Fail)]
pub enum Badness {
  #[fail(display = "Ze badness")]
  Level(String)
}

pub fn do_badly() -> Result<(), Error> {
  Err(Badness::Level("much".to_owned()).into())
}

#[test]
pub fn get_badness() {
  match do_badly() {
    Err(Badness::Level(level)) => panic!("{:?} badness!", level),
    _ => (),
  };
}

fails with

error[E0308]: mismatched types
  --> barsa-nagios-forwarder/src/main.rs:74:9
   |
73 |   match do_badly() {
   |         ---------- this match expression has type `failure::Error`
74 |     Err(Badness::Level(level)) => panic!("{:?} badness!", level),
   |         ^^^^^^^^^^^^^^^^^^^^^ expected struct `failure::Error`, found enum `Badness`
   |
   = note: expected type `failure::Error`
              found type `Badness`

How can I formulate a pattern which matches a specific custom error?

Upvotes: 0

Views: 379

Answers (1)

user6564029
user6564029

Reputation:

You need to downcast the Error

When you create a failure::Error from some type that implements the Fail trait (via from or into, as you do), you temporarily hide the information about the type you're wrapping from the compiler. It doesn't know that Error is a Badness - because it can also be any other Fail type, that's the point. You need to remind the compiler of this, the action is called downcasting. The failure::Error has three methods for this: downcast, downcast_ref and downcast_mut. After you've downcast it, you can pattern match on the result as normal - but you need to take into account the possibility that downcasting itself may fail (if you try to downcast to a wrong type).

Here's how it'd look with downcast:

pub fn get_badness() {
    if let Err(wrapped_error) = do_badly() {
        if let Ok(bad) = wrapped_error.downcast::<Badness>() {
            panic!("{:?} badness!", bad);
        }
    }
}

(two if lets can be combined in this case).

This quickly gets very unpleasant if more than one error type needs to be tested, since downcast consumes the failure::Error it was called on (so you can't try another downcast on the same variable if the first one fails). I sadly couldn't figure out an elegant way to do this. Here's a variant one shouldn't really use (panic! in map is questionable, and doing anything else there would be plenty awkward, and I don't even want to think about more cases than two):

#[derive(Debug, Fail)]
pub enum JustSoSo {
    #[fail(display = "meh")]
    Average,
}

pub fn get_badness() {
    if let Err(wrapped_error) = do_badly() {
        let e = wrapped_error.downcast::<Badness>()
            .map(|bad| panic!("{:?} badness!", bad))
            .or_else(|original| original.downcast::<JustSoSo>());
        if let Ok(so) = e {
            println!("{}", so);
        }
    }
}

or_else chain should work OK if you actually want to produce some value of the same type from all of the possible\relevant errors. Consider also using non-consuming methods if a reference to the original error is fine for you, as this would allow you to just make a series of if let blocks , one for each downcast attempt.

An alternative

Don't put your errors into failure::Error, put them in a custom enum as variants. It's more boilerplate, but you get painless pattern matching, which the compiler also will be able to check for sanity. If you choose to do this, I'd recommend derive_more crate which is capable of deriving From for such enums; snafu looks very interesting as well, but I have yet to try it. In its most basic form this approach looks like this:

pub enum SomeError {
    Bad(Badness),
    NotTooBad(JustSoSo),
}

pub fn do_badly_alt() -> Result<(), SomeError> {
    Err(SomeError::Bad(Badness::Level("much".to_owned())))
}

pub fn get_badness_alt() {
    if let Err(wrapper) = do_badly_alt() {
        match wrapper {
            SomeError::Bad(bad) => panic!("{:?} badness!", bad),
            SomeError::NotTooBad(so) => println!("{}", so),
        }
    }
}

Upvotes: 2

Related Questions