Is Result<Result<Result<Ans, Eval_Err>, Json_Err>, Network_Err> a code smell?

I have a certain function that right now returns:

fn remote_eval<T: Remote_Eval_Trait>(t: T) -> Result<Result<Result<T::Ans, Eval_Err>, Json_Err>, Network_Err>

Why? When we try to eval t: T on a remote machine, we can run into a number of errors:

Error in the network. Error in the json encoding. Error in the evaluation itself, etc ...

So now I have this triply nested Result.

One possible solution to this is:

pub enum MyErr {
  Network_Err(..),
  Json_Err(..),
  Eval_Err(..)
}

fn remote_eval(t: T) -> Result<T, MyErr>

But this enum feels a bit weird, like duct taping over a problem rather than addressing the issue.

What is the idiomatic way of dealing with this ?

1 Like

And nevertheless that's exactly the approach which crates like thiserror do endorse.

Another way is, of course, to just Box<dyn Error> anything (or use anyhow, if you want something more ergonomic).

8 Likes

Short answer: yes. The Result type is meant to be used following a monadic pattern, so you are supposed to flatten it.

8 Likes

Not only thiserror but also the language itself kind-of endorses it, in that

  • combinators of Result, and ? syntax are designed to make handling a single layer of Result most ergonomic
  • also, the ? syntax supports From conversions for the reason of error upcasting, e.g. converting to Box<dyn Error> or similar, but alternatively also to such an enum type, conveniently, provided you give it the necessary From implementations (use the #[from] attribute of thiserror to generate such impls automatically; then you can just use ? operator on Result<_, EvalErr>, Result<_, Json_Err>, or Result<_, Network_Err>, too, inside of the remote_eval function body).
8 Likes

I've seen an approach of keeping two levels of Result, one for the kind of errors which should be kept private and one the public "domain" facing errors.

Talk in french, bad audio recording.
image

I don't necessarily agree, but I still think about it.

1 Like

Taking this advice to the limit, it seems the approach is to either (1) through thiserror or (2) through manual code, to build something like:

pub enum My_Crate_Err {
  ... and list all the possible errors the crate can use ...
}

?

Well, it's entirely possible that you want to have not a single enum, but a hierarchy of them - each "layer" of the logic will have an enum, each variant of which holds inside another enum from lower layer.

3 Likes

I think we once discussed that a possible outcome could be that a library's functions should each return the most specific error possible and that maybe that library could define an error enum encapsulating all of its possible errors for the user's convenience.

2 Likes

Do you have a link to the discussion? I think I previously asked (here or elsewhere) a question regarding {anyhow, thiserror, Result} but I can't find it searching for {anyhow, thiserror, Result, @erelde, @zeroexcuses }.

It seems it was my idea to do this

There are lots of discussion about this topic :upside_down_face: lots to read and reason about

2 Likes

I found this to be a pretty good summary of error handling in Rust. My big takeaway was that thiserror may be nice, but the extra effort to write your own full enum could be worth it to save compile time.

I did a search for 'compile', and all I got was

Compared to the previous version, our new code is a lot more specific. Users now get a lot more insight into the possible error cases that might be returned. As an added benefit, we also no longer have to Box Error because the size of WordCountError can be determined at compile time.

Where does it take about compile time of enum vs thiserror ? Compile time is very important to me.

Regarding this pattern, I made the woah crate to help make this pattern easy!

1 Like

The point is that sometimes it's better if the implementations thiserror would generate for you are written by hand, so that compiler doesn't spend its time on their generation.

Ahh, here was the bit at the end about it. Blame the proc macro process.

I think the examples given of "internal errors" on that slide are right on the edge of where I would instead panic, depending on the kind of package I'm building and what it means that the error happened: an unexpected None probably means my crate has a bug, and a panic is better there, while invalid data probably means something else has messed with the file / DB, so a specific error saying that is more helpful. Overall, I'm not sure there's enough room between panics and semantic errors to fit internal errors.