Dmitrii Aleksandrov

Why Use Structured Errors in Rust Applications?

Posted on Last edited 8 mins

Error Handling Rust Tech

TL;DR I prefer thiserror enums over anyhow, even for application code that simply propagates errors. Custom error types require additional effort, but make the code easier to reason about and maintain down the road.

“Using thiserror for libraries and anyhow for applications”

In 2025, this is the conventional error handling practice in the Rust community. Let’s quickly recap the main ideas:

  • Libraries should expose error types that provide as much information as possible. This allows the callers to detect and handle specific error conditions. A specific error code is more useful than “Oops, something went wrong”. Structured 1 error types facilitate statically-checked pattern matching, which is more reliable than runtime reflection, downcasting, or regex-matching an error message. They also provide nice autogenerated docs, which is very important for libraries.
  • Unfortunately, propagating structured errors in Rust is associated with the boilerplate of manually writing out wrapper error types, their impls and conversions. 2
  • thiserror removes the boilerplate around implementing Display , Error and From for your error types. Using it is a no-brainer for libraries that can “afford” proc macro dependencies. Although, you still need to define and annotate the wrapper types themselves, which there are usually plenty.
  • End applications often don’t care about the specific cause of an error and simply propagate errors up the stack until they reach some kind of a “catch-all” point. For this use case, even thiserror-assisted error types are often considered too cumbersome and not worth it.
  • Instead, application developers commonly use anyhow . It provides a “universal” dynamic error type, along with helper methods that make dealing with it even easier than returning a string. 3

If you google “error handling in Rust”, the typical introductory posts are going to start with the basics of panic, Option and Result, then explain these two libraries, give this rule of thumb to choose bethween them, show some code, and stop right here.

Pattern matching isn’t the only reason to use structured errors

At work, my team has written a ~50k LoC web server in Rust. We’ve been maintaining it full-time for over a year. It mostly follows the same pattern of “just propagate the error” and almost never pattern-matches specific error conditions. The main benefit of structured errors is not applicable. It sounds like we should be using something like anyhow instead of spending time on maintaining our own error types. But I found many other benefits of doing so:

The tradeoffs

Custom errors have their drawbacks:

  • It’s more code and more types 6.
  • To combat the boilerplate, you usually introduce a third-party dependency like thiserror. 7
  • You need to put thought into structuring the code, because otherwise no one will find and reuse your existing error types.
  • You need to come up with names for error types and enum variants. These names may not be as clear as full error messages. So, in order to understand the code, you may end up jumping to the message anyway. I requested a feature in rust-analyzer that would allow previewing #[error(..)] attributes without jumping away from the code that I’m working on.
  • You also need to maintain these names as the codebase evolves! In my team, we often forgot to update the type/variant name when updating an error message.
  • You have to maintain the error types as your functions begin or stop returning certain error variants. This adds maintainance overhead, but can be seen as an advantage (see the points about code reviews, descriptive interfaces, enforcing context on errors). I see a parallel with adding/removing Result from the signature, which is widely considered a “necessary evil”.
  • If your error is in a separate file, you jump back-and-forth between the files when coding. 8
  • If an error enum is public in its crate, the compiler doesn’t warn about unused variants. This means that sometimes unused variants may accumulate and you need to manually notice and trim them. 9
  • If you want your errors to include a backtrace, you need to explicitly add a Backtrace field. 10

If your application is performance-sensitive, there are also performance considerations that don’t present a clear winner:

  • Should your errors be cheap to create? Are your errors frequently discarded without ever being displayed? Are your errors displayed multiple times? Do you need a backtrace? Currently, the contents of anyhow::Error are heap-allocated, the arguments to anyhow! are eagerly formatted , and a backtrace may be captured depending on environment variables11. Typical custom errors avoid allocations, are lazily formatted, and don’t capture a backtrace. But you can configure all of that.
  • Should your errors be small and cheap to propagate? Currently, anyhow::Error is always a single pointer, while custom errors range from zero-sized unit structs to large collections of values that are waiting to be formatted. You may need to box and/or preformat your custom errors.

Custom errors can even have surprising downsides in libraries (e.g., semver-related ). But I don’t duscuss libraries today!

Assessing the tradeoff is up to you. Structured errors are worth it in my application.

To be continued

To present the full picture, I also wanted to cover:

But this saga takes too long, so these topics go to their own separate posts and this post ends here. 🚧

I have to be ✨ agile ✨ in order to actually publish anything.


I’ve found these good posts about structured errors in Go and Haskell, but they’re quite detailed and the actual approach in the code is quite different from mine:

I’ll discuss mine in a future post. See also my other posts about error handling:

  1. “Rust Solves The Issues With Exceptions”
  2. “Why Use Structured Errors in Rust Applications?”
  3. “Go Didn’t Get Error Handling Right”

Discuss


  1. “Structured errors” = non-opaque, non-string error types. Types that have a “shape”. ↩︎

  2. This is caused by having a nominal type system in general and lacking anonymous unions in particular. ↩︎

  3. Returning an error string is a common pattern among beginners, because it’s very easy to understand and use. anyhow retains the same advantages while also supporting richer features like backtraces, error chaining and automatic conversions from library error types. ↩︎

  4. Actually, error messages are specified in the Display impl, rather than the type itself. But when using thiserror, the error messages appear as annotations on the type. ↩︎

  5. While most of this post comes from my real experience, the examples in this list are theoretical. I’ve never actually implemented these features. Please call me out if something’s wrong with this section. Also, send concrete examples, if you know any! ↩︎

  6. IMO, it’s totally worth it, because these types replace hand-written docs that list the possible errors. Type-checked docs are the best! ↩︎

  7. For dynamic errors, you usually introduce an anyhow-style dependency too. But if you want to avoid dependencies, then living with raw Box<dyn Error> is probably easier than hand-writing impls for all your custom types. ↩︎

  8. I rarely hit this issue in practice, because I try to keep my error types local to the function. I’ll discuss the factoring and placement of error types in the next post in the series. ↩︎

  9. Just like with the point about “jumping to the error message”, one can argue that this is just a limitation of our current tooling, rather than a fundamental flaw of custom enums. ↩︎

  10. I’ve never needed backtraces in my own errors, because good error messages with enough context are enough to debug and reproduce the issue. And custom errors encourage thinking about context messages. Both you and your users benefit from this. ↩︎

  11. Currently, capturing backtraces is expensive. See backtrace-related issues in the anyhow repo. ↩︎

Comments