1

I am working on a project in which I'm using methods from a number of crates Foo1, Foo2, etc., returning objects like Result<T,Foo1Error>, Result<T,Foo2Error>, respectively, within an implementation of a trait from another crate Bar, whose methods must return Result<U,BarError>. Because Foo1, Foo2, and Bar are all external, I cannot implement impl From<Foo1Error> for BarError or ditto for for Foo2Error due to the orphan rule. The popular third-party crates for error handling (error_chain, thiserror, etc.) don't look to me like they help, since they are generally for creating new error types and I'm stuck with having to return BarError. So, I've defined functions like foo1err_to_barerr and foo2err_to_barerr and my code is littered with dozens of chains involving .map_err(foo1err_to_barerr)?.

While it works perfectly well, this visually clutters up the logical parts of the code quite a bit. Are there any alternative techniques that move some of the error mapping boilerplate out of the logical parts of code? If I only had Foo1Error to worry about, for each trait method I'm implementing I'd just make separate method that returned Result<U,Foo1Error> and then all the trait method would do would map the error (collapsing multiple map_err calls into one). But, I have multiple external error types to deal with, and so that new helper method would have to return a boxed error, or a custom error enum, and between the duplication of methods and new compound error types I think I wouldn't (personally) prefer that style any more.

If I permit myself to dream up some blue sky syntax (of course not something Rust supports) would be the ability to define error mapping that happens when the function scope exits: in the meat of the method I could just use ? to my heart's content, and then state once how each error type coming from a ? has to map to the returned Result's error type.

I kept everything above generic, but in case there is a particular solution rather than a general one, the "Bar" crate is actually tonic and BarError is actually tonic::Status.

2 Answers 2

3

As you mentionned, the orphan-rule prevents you from implementing From between those types provided by other crates, but if you introduce you own pivot type (in your own crate) you will have the ability to perform two conversions in a row.

Please find below an attempt to illustrate this idea.
The drawback is that you need a function call for each level of ? convenient conversion: one from Foo1Err to your pivot type (AnyFooErr here), and another from your pivot type to BarErr.
I chose to hide this in an inner function (as shown in _work_expended()), but we still have to repeat the function prototype twice (with different error types, obviously).
Eventually, a macro could help avoid repeating the function prototype (see how via_any_foo_err! expands work() to something like _work_expended()), but I don't like this very much since (at least in my editor) the code formatter does not handle the code inside the macro.

#[derive(Debug)]
struct Foo1Err {
    f1_msg: String,
}

fn foo1_fnct(i: i32) -> Result<i32, Foo1Err> {
    if i % 2 != 0 {
        Ok(2 * i)
    } else {
        Err(Foo1Err {
            f1_msg: format!("{} is multiple of 2", i),
        })
    }
}

#[derive(Debug)]
struct Foo2Err {
    f2_msg: String,
}

fn foo2_fnct(i: i32) -> Result<i32, Foo2Err> {
    if i % 3 != 0 {
        Ok(3 * i)
    } else {
        Err(Foo2Err {
            f2_msg: format!("{} is multiple of 3", i),
        })
    }
}

#[derive(Debug)]
struct BarErr {
    bar_msg: String,
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

#[derive(Debug)]
enum AnyFooErr {
    F1(Foo1Err),
    F2(Foo2Err),
}

impl From<Foo1Err> for AnyFooErr {
    fn from(err: Foo1Err) -> Self {
        Self::F1(err)
    }
}

impl From<Foo2Err> for AnyFooErr {
    fn from(err: Foo2Err) -> Self {
        Self::F2(err)
    }
}

impl From<AnyFooErr> for BarErr {
    fn from(err: AnyFooErr) -> Self {
        Self {
            bar_msg: match err {
                AnyFooErr::F1(err) => format!("from Foo1Err: {}", err.f1_msg),
                AnyFooErr::F2(err) => format!("from Foo2Err: {}", err.f2_msg),
            },
        }
    }
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

macro_rules! via_any_foo_err {
    (fn $f_name:ident(
        $( $p_name:ident: $p_type:ty ),*
    ) -> Result<$r_type:ty, $e_type:ty> $body:block) => {
        fn $f_name(
            $( $p_name: $p_type ),*
        ) -> Result<$r_type, $e_type> {
            fn inner(
                $( $p_name: $p_type ),*
            ) -> Result<$r_type, AnyFooErr> $body
            Ok(inner($( $p_name ),*)?)
        }
    };
}

via_any_foo_err! {
    fn work(i: i32) -> Result<i32, BarErr> {
        let i1 = foo1_fnct(i)?;
        let i2 = foo2_fnct(i)?;
        Ok(i1 + i2)
    }
}

fn _work_expended(i: i32) -> Result<i32, BarErr> {
    fn inner(i: i32) -> Result<i32, AnyFooErr> {
        let i1 = foo1_fnct(i)?;
        let i2 = foo2_fnct(i)?;
        Ok(i1 + i2)
    }
    Ok(inner(i)?)
}

fn main() {
    for i in 0..10 {
        match work(i) {
            Ok(r) => println!("{} ~~> {}", i, r),
            Err(e) => println!("{}: {}", i, e.bar_msg),
        }
    }
}
/*
0: from Foo1Err: 0 is multiple of 2
1 ~~> 5
2: from Foo1Err: 2 is multiple of 2
3: from Foo2Err: 3 is multiple of 3
4: from Foo1Err: 4 is multiple of 2
5 ~~> 25
6: from Foo1Err: 6 is multiple of 2
7 ~~> 35
8: from Foo1Err: 8 is multiple of 2
9: from Foo2Err: 9 is multiple of 3
*/
Sign up to request clarification or add additional context in comments.

Comments

1

serde has a similar problem to solve: How do you implement [De-]Serialize for external types. You can more or less use the same approach, insert a delegate of sorts as intermediator between Foo1::Error and Bar::Error:

pub struct IntermediateError;

// This implementation can be automatically generated by `thiserror` or a similar crate.
impl From<foo1::Error> for IntermediateError {
    fn from(_: foo1::Error) -> Self {
        Self
    }
}

// This could be generated by a macro as well if the definition of `bar::Error` and `IntermediateError` match, see serde remote derive for limitations.
impl From<IntermediateError> for bar::Error {
    fn from(_: IntermediateError) -> Self {
        Self
    }
}

You can then write your function as one returning IntermediateError without any calls to .map_err(foo1err_to_barerr), instead you wrap the whole function with the implementation with a thin wrapper function wich maps IntermediateError to bar::Error once:

fn with_intermediate_error(_: ()) -> Result<(), IntermediateError> {
    Ok(foo1::foo()?)
}

pub fn with_bar_error(args: ()) -> Result<(), bar::Error> {
    Ok(with_intermediate_error(args)?)
}

IntermediateError should closely mirror the definition of bar::Error

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.