Why Use Structured Errors in Rust Applications?
18 points by runxiyu
18 points by runxiyu
How I often write application code is I start with anyhow
-like error handling, and then once I find a place where I need to do more fine-grade error handling I move to structured errors.
Also, error handling in Rust is not a solved problem, there are other crates than thiserror
and anyhow
like snafu or miette.
I requested a feature in rust-analyzer that would allow previewing #[error(..)] attributes without jumping away from the code that I’m working on
Going to definition in a split is such a convenient tool for these sorts of workflows:
https://matklad.github.io/2024/10/08/two-tips.html#Split-And-Go-To-Definition
Wish I’ve found that workflow years earlier.
My favorite out of the approaches I’ve seen is what is happening in Roc: structurally typed enums (polymorphic sum types) with inferred variants. Just create a error tag name at the throw point and the compiler will track it through the program for you. Lightweight and easy yet has the advantages of structured errors.
I don’t think structural types are even on the roadmap for Rust though.
It’s a bit of a pitty Zig couldn’t use a tagged union instead of a bare enum (without custom payload) for its polymorphic error enum. There was an issue about it (https://github.com/ziglang/zig/issues/2647) and AFAICT the recommendation seems to be to have the caller pass in an error context and have the callee mutate and set it to a given variant when a particular error is thrown, which the caller has to manually unwrap given the error code. It sounds like more boilerplate and risk than just using the approach in this article (and I’d even wonder in Zig if its easier to make your own result types and not use the nice error syntax at all, than to create and pass around all this mutable context?).
You can kind of get a similar experience if you have several specific Error
types and a wrapper Error
enum that implements From<QuxError>
for each error type you want to handle. It’s not as lightweight as Roc, but if you’re using thiserror
you can slap #[error(transparent)] Qux(#[from] QuxError)
in your enum and forget about it, or implement the trait yourself if you want finer-grained control over what ends up in the generic Error
enum.
In the larger Rust applications I’ve worked on I found this to be a pretty good method to keep the complexity of error handling down: each module that’s important enough implements its own Error
, and then at the root we had a generic Error
with all the impl From
s that also provided a bespoke Display
implementation that gave us enough context to debug the error when it was logged.