I'm trying to understand the pro's and cons of each of these approaches.
It seems like "Box"-ing is less code & more flexible when encountering a new error type, but incurs the cost of moving errors on to the heap and dynamic dispatch. Are there other downsides?
I noticed libraries like reqwest and tools I consider to be fast like ripgrep use Boxing, so it seems like any performance cost of dynamic dispatch shouldn't concern me, so is it more of a correctness / explicitness tradeoff?
I'd love to hear other people's opinions / preferences / guidelines on how they make this decision.
(I put amore verbose version of this with code examples here )
Well, constructing Errs is usually more rare than constructing the Ok variant, because most operations succeed, don't fail. So the runtime performance of error handling is usually not critical, or not even relevant at all.
In the case of ripgrep, the tool gets its enormous performance advantage over classical grep by parallelizing search and by doing less work (ignoring hidden/.gitignore'd files) by default. The time spent in constructing Results is probably several orders of magnitude smaller than constructing and executing state machines, not to mention reading all those files from diskā¦
I don't think there are 3 approaches. "Wrapping errors" and "defining your own errors" is essentially the same thing: non-boxed errors. From this point of view, it doesn't matter whether your enum Error contains associated data from other crates or you make it hold types purely from your own code. It's still all statically-typed without additional heap allocation.
The usual trade-off between the two approaches is that using a boxed trait object loses typing information, so it's easier on the implementor and less useful for the user, meanwhile preserving full static types requires writing slightly more boilerplate code on the implementor's part, while being somewhat more useful to the user.
I have previously explained my views w.r.t. very strongly-typed error handling being somewhat overrated in the Rust community; apart from std::io::ErrorKind::Interrupted I have never seen any real use-case where it would have been possible (let alone necessary) to act differently on different kinds of errors coming from a given, narrow-focused crate, apart from printing, bubbling, or unwrapping it.
Therefore, while somewhat more elegant in principle, I don't think you have to necessarily define your own error enum. Creating a newtype around a Box<dyn Error> can still be genuinely useful, though, because it allows other authors of downstream crates to provide From impls based on your error type.