I cautiously agree, with the caveat that while I thought I would really like Rust's error handling, it has been painful in practice. I'm sure I'm holding it wrong, but so far I have tried:
* thiserror: I spend ridiculous and unpredictable amounts of time debugging macro expansions
* manually implementing `Error`, `From`, etc traits: I spend ridiculous though predictable amounts of time implementing traits (maybe LLMs fix this?)
* anyhow: this gets things done, but I'm told not to expose these errors in my public API
Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?
And when I ask these questions to various Rust people, I often get conflicting answers and no one seems to be able to speak with the authority of canon on the subject. Maybe some of these questions have been answered in the Rust Book since I last read it?
By contrast, I just wrap Go errors with `fmt.Errorf("opening file `%s`: %w", filePath, err)` and handle any special error cases with `errors.As()` and similar and move on with life. It maybe doesn't feel _elegant_, but it lets me get stuff done.
FWIW `fmt.Errorf("opening file %s: %w", filePath, err)` is pretty much equivalent to calling `err.with_context(|| format!("opening file {}", path))?` with anyhow.
What `thiserror` or manually implementing `Error` buys you is the ability to actually do something about higher-level errors. In Rust design, not doing so in a public facing API is indeed considered bad practice. In Go, nobody seems to care about that, which of course makes code easier to write, but catching errors quickly becomes stringly typed. Yes, it's possible to do it correctly in Go, but it's ridiculously complicated, and I don't think I've ever seen any third-party library do it correctly.
That being said, I agree that manually implementing `Error` in Rust is way too time-consuming. There's also the added complexity of having to use a third-party crate to do what feels like basic functionality of error-handling. I haven't encountered problems with `thiserror` yet.
> Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?
If you wish to make sure it's not a breaking change, mark your enum as `#[non_exhaustive]`. Not terribly elegant, but that's exactly what this is for.
Hope it helped a bit :)
> Beyond these concerns, I also don't love enums for errors because it means adding any new error type will be a breaking change. I don't love the idea of committing to that, but maybe I'm overthinking?
Is it a new error condition that downstream consumers want to know about so they can have different logic? Add the enum variant. The entire point of this pattern is to do what typed exceptions in Java were supposed to do, give consuming code the ability to reason about what errors to expect, and handle them appropriately if possible.
If your consumer can't be reasonably expected to recover? Use a generic failure variant, bonus points if you stuff the inner error in and implement std::Error so consumers can get the underlying error by calling .source() for debugging at least.
> By contrast, I just wrap Go errors with `fmt.Errorf("opening file `%s`: %w", filePath, err)` and handle any special error cases with `errors.As()` and similar and move on with life. It maybe doesn't feel _elegant_, but it lets me get stuff done.
Nothing stopping you from doing the same in Rust, just add a match arm with a wildcard pattern (_) to handle everything but your special cases.
In fact, if you suspect you are likely to add additional error variants, the `#[non_exhaustive]` attribute exists explicitly to handle this. It will force consumers to provide a match arm with a wildcard pattern to prevent additions to the enum from causing API incompatibility. This does come with some other limitations, so RTFM on those, but it does allow you to add new variants to an Error enum without requiring a major semver bump.
If you're willing to do what you're saying in Go, exposing the errors from anyhow would basically be the same thing. The only difference is that Rust also gives all those other options you mention. The point about other people saying not to do it doesn't really seem like it's something you need to be super concerned with; for all we know, people might tell you the same thing about Go if it had the ability for similar APIs, but it doesn't
> I also don't love enums for errors because it means adding any new error type will be a breaking change
You can annotate your error enum with #[non_exhaustive], then it will not be a breaking change if you add a new variant. Effectively, you enforce that anybody doing a match on the enum must implement the "default" case, i.e. that nothing matches.
You have to chill with rust. Just anyhow macro wrap your errors and just log them out. If you have a specific use case that relies on using that specific error just use that at the parent stack.
I personally like the flexibility it provides. You can go from very granular with an error type per function and an enum variant per error case, or very coarse with an error type for a whole module that holds a string. Use thiserror to make error types in libraries, and anyhow in programs to handle them.
I will at least remark that adding a new error to an enum is not a breaking change if they are marked #[non_exhaustive]. The compiler then guarantees that all match statements on the enum contain a generic case.
However, I wouldn't recommend it. Breakage over errors is not necessarily a bad thing. If you need to change the API for your errors, and downstreams are required to have generic cases, they will be forced to silently accept new error types without at least checking what those new error types are for. This is disadvantageous in a number of significant cases.