I have a few questions about error handling in 2023.
What is the right way to attach a path to an io::Error? A NamedIoError variant with a PathBuf and the error? This seems like a really common problem; is there a wheel I can avoid reinventing?
When is it appropriate to use source vs from in thiserror? If an error variant contains a T: Error, when should that T instance be returned by the source method? Relatedly, how should I think of anyhow contexts?
What errors should have backtraces attached? What about async code?
Are there antipatterns to avoid? Are there libraries with particularly good practices?
Is it better to have more error types with fewer variants or the opposite? Should a library have one big error type or is it better to restrict each fallible function to return a type that expresses exactly what errors can occur?
I don't think there's a single particular "right" way.
#[from] implies #[source]. I personally never found a compelling argument for using #[source] over #[from], because the From impl for an underlying error is good to have for easier ?-bubbling. Maybe if there's already a manual impl and it would cause a conflict, then you will find #[source] useful.
Always? I mean, knowing the underlying cause is always better than not knowing it.
In what sense?
Probably as many of them as you can achieve. Again, knowing where an error comes from is better than not knowing it.
Well, what about it?
In principle, restricting errors to only the variants that occur would be ideal. In practice, this doesn't really gain you much usually, and it's pretty annoying to implement, so most code bases end up with a single error type. Unless there is an obvious separation into a very small number of cases, e.g. serialization formats might come with separate ser::Error and de::Error types.
Regarding contexts, is adding a context to an error creating a new error or augmenting an existing error with some optional data? There are cases where both of these options make sense. For a library should I just stuff some context information into an extra Option<String> carried around by an error?
Regarding async, is there something I can or should add beyond a backtrace that has additional useful information? An actual backtrace looks very different than the code because of how the code is transformed by the compiler.