After having written countless implementations of From to convert various types of Error (required for using ?), I wanted to solve the problem once and for all by creating Snag, an implementation of std::error::Error with a generic conversion of anything implementing std::error::Error to Snag.
error[E0119]: conflicting implementations of trait `std::convert::From<Snag>` for type `Snag`
--> src/lib.rs:43:1
|
43 | impl<E: Error + 'static> From<E> for Snag {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: conflicting implementation in crate `core`:
- impl<T> std::convert::From<T> for T;
For more information about this error, try `rustc --explain E0119`.
Thanks for the suggestions, but I don't think any will work. Here is my use case:
I'm writing some crate top_crate.
From function top_func in my crate top_crate, I'm calling a function dep_func that lives in dep_crate that returns a Result<T, DepError>, where DepError lives also in dep_crate and implements std::error::Error.
So I want to use a ? after dep_func, which only works if top_func returns some Result<T, E>, where there is an impl From<DepError> for E.
Since I don't want to keep re-inventing the wheel, I create a crate snag that contains Snag which implements std::error::Error, to be used in all my crates as the universal error. Now I want to provide From<E> for Snag for all other errors. Due to orphan rule, that impl can only live in crate snag.
Regarding the suggestions:
I don't see how to combine this with ?
I can provide a trait Snagifiable in snag, but then due to orphan rule, it cannot be implemented for DepError in top_crate.
If I want to convert DepError first to SnagError and then to Snag, I don't know how to do this with ?.
None of them are great, but here is how to use each of them at the call site:
// 1. Use `Wrapper` to do the conversion.
let foo = dep_result.map_err(Wrapper)?;
// 2. Use `MyFrom` to do the conversion.
let foo = dep_result.map_err(Snag::my_from)?;
// 3. Use `SnagError`.
let foo = dep_result.map_err(Snag::from)?;
The anyhow crate is providing similar functionality, since it can convert any Error to anyhow::Error. But anyhow::Error does not implement the std library Error trait! So that probably confirms that you can't implement Error for Snag as well as From<Error> for Snag.
If I remove Error for Snag it resolves that problem, but there is another problem remaining: From<Error> for Snag conflicts with From<String> for Snag:
error[E0119]: conflicting implementations of trait `From<std::string::String>` for type `Snag`
--> src/lib.rs:58:9
|
46 | impl From<String> for Snag {
| -------------------------- first implementation here
...
58 | impl<E: Error + 'static> From<E> for Snag {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Snag`
|
= note: upstream crates may add a new impl of trait `std::error::Error` for type `std::string::String` in future versions
And looking again at anyhow, it also does not implement From<String> for anyhow::Error.
Using dedicated methods for converting from String/@str to Snag, it compiles:
And to close the loop thiserror is the other side where it derives std::error::Error and From<SomeOtherError> for your own types (more useful for libraries where they want users to be able to match the specific error)
That also can't derive arbitrary From<Error>, for the same reasons.
I guess I will just keep doing what I have been doing so far: each of my crates will have its own Error which implements std::error::Error, and a dozen or so From implementations, one for every E in every Result<T, E> I receive from dependencies and handle with ?.
And it will be copy-pasted and tweaked from an earlier crate I wrote. I feel I'm becoming the expert in writing simple Error implementations!
At least I can now do so with a clear conscience, knowing that there is no obviously better way.
Just for completing the conversation, why not use the code I posted? It does automatically convert from any Error to Snag. It just doesn't automatically convert from String/@str to Snag, and you can't implement Error for Snag (which isn't required by Result). So at least it will save writing most of the conversions. And instead of Snag you could have a specific error type per crate.
The requirement you dropped, but which I would like to keep, is that my error type should implement std::error::Error.
Why? Because it is a convention that others would expect. I would expect it for any E in Result<T, E>. Also, because std::error::Error::source requires it - this could affect others trying to use my Error as source, but also, one instance of my Error may have another instance of my Error as a source.
I understand, that's perfectly reasonable and I'm not trying to convince you to omit the Error impl.
But there is a counterpoint: I don't use anyhow, but I think it's interesting that it omits the Error impl (since it's impossible) and is still widely used. One reason might be that its AsRef and From implementations make it easy to convert from anyhow::Error to &(dyn Error + 'static). This would allow anyhow::Error to be easily used as the source for someone else's Error impl.
Note that there are exceptions to this even within std. Rc::downcast::<T>, for example, returns Result<Rc<T>>, Self> so that you can try a failed downcast again with a different T.
It may not be as expected as you think. In addition to not being required for Result, it's typically not required for traits that have associated error types either, like FromStr say. And a number of types that are commonly used as errors do not implement Error, and instead implement Into<Box<dyn Error>> -- including not just third-party types like anyhow::Error, but also language/std types like &str and String.
In particular, Box<dyn Error> itself does not implement Error.
I concur, it [1] is one of the first crates I add when moving out of prototyping to “I should properly handle errors” in any project.
Just adding a #[from] annotation to a field in your enum variant is all it takes to convert foreign error types into your own. What I really appreciate is that it makes my errors self documenting. I can see all the ways a function can fail from its signature alone.
I've taken a look at thiserror more than once, and it looks useful, but its value doesn't reach my threshold for including it. It appears it would help me creating some Display and From implementations, but these are usually already so simple, it is faster to write them from scratch than remembering how to do them with thiserror.
Interestingly enough, I find myself in exactly the same position as you. A friend pointed me to thiserror recently, and my pros and cons look a lot like yours.
And it's true, if you're already creating enum errors with docstrings and From impls for variants, functionally using thiserror wouldn't change all that much.
But I'd also like to add that it would be just a matter of getting used to thiserror, similarly to how you (presumably) got used to using serde's attribute macros in order to get (mostly) declarative dr/serialization.
Once used to it, it's like second nature, and it does have the advantage of lifting the code to a purely declarative level, which is always nice.
Hell I think I just talked myself into using thiserror after all.
FWIW, it's just #[error(some message)] on the struct or each variant for the Display and #[from] on the field you want the From to be generated for, but the latter does have eg. some interesting interaction with #[source] that can be hard to remember.
Like most derives, the bigger deal isn't that the implementation of what it generates is harder to write, but that it's harder to read. There's a lot to be said for the compactness of the thiserror representation, and particularly for how easy it is to keep the messages in sync with changes to the structure.
But - I'm not your boss, write code however you feel most comfortable.