The definitive guide to error handling in Rust

ARCHITECTURE
RUST
ERROR HANDLING

Learn to model and handle any error using idiomatic Rust.

Illustration of a seagull attacking a crab

Share better Rust!

The Y Combinator logoThe Reddit logoThe X logo

Contents

🦀🦀🦀

Introduction

Are you overwhelmed by the amount of choice Rust gives us for handling errors? Confused about when to return a structured error type or a Box<dyn Error>? Intimidated by Box<dyn Error + Send + Sync + 'static>'s beefy type signature?

Whether you're building an application or library, this guide will help you make the right decision.

I love error handling. I'm obsessed. I work in the finance and space industries, and things go wrong a lot.

Failure cases vastly outnumber success cases. Knowing how to communicate what went wrong, to the right audience, in an appropriate amount of detail is a skill that sets you apart from other developers.

Think about how great the Rust compiler's error messages are compared to other programming languages. We want users of our code to have that same reaction, whether they're on our team or using our library. We want them to be impressed when things go wrong!

Before we dazzle anyone with our error handling skills, though, let's nail the fundamentals.

🦀🦀🦀

Part I

Rust error handling basics

What is an error in Rust?

In Rust, an error is any type that implements the std::error::Error trait. Here's the definition:

rust
src/core/error.rs
pub trait Error: Debug + Display { // Provided methods
fn source(&self) -> Option<&(dyn Error + 'static)> { ... }1
fn description(&self) -> &str { ... } fn cause(&self) -> Option<&dyn Error> { ... } fn provide<'a>(&'a self, request: &mut Request<'a>) { ... }}

This is a moderately threatening trait definition, but all four of these methods have default implementations provided for us.

Any type that implements both Debug and Display can implement Error. There's very little manual work required.

In fact, Error::cause and description are deprecated in favor of Error::source and the Display implementation, respectively. You should never have to worry about them, except when working with older code.

Error::provide is part of an experimental nightly build, so I won't discuss it here. You won't have to worry about it unless you're working with cutting-edge, unstable code.

The return type of Error::source warrants closer examination 1, because we'll see similar types throughout this guide.

You know what Option is already. &(dyn Error + 'static) simply means "a reference to some error that may live for the whole duration of the program".

The 'static lifetime is important for error handling, because errors are often handled long after the code that causes them returns, sometimes on a different thread.

Good luck handling an error that's been dropped unexpectedly! Rust protects us from this scenario.

You'll often see 'static alongside Send and Sync bounds. dyn Error + Send + Sync + 'static describes "some error that can live as long as the program, be sent between threads by value or shared across threads by immutable reference".

Error::source's return type, &(dyn Error + 'static), doesn't make any promises about thread safety.

In general, standard library code places more relaxed bounds on dynamic errors than you'll see in the broader ecosystem and use in your own projects.

This allows the widest variety of things to behave as errors, with stricter requirements left to the user's discretion.

How do we make an Error type static? Simple – use only owned fields, or fields which specify the 'static lifetime for references and trait objects.

The following type is only 'static if the reference assigned to field happens to be 'static itself:

rust
pub struct QuestionablyStatic<'a> { field: &'a str,}

These are always 'static:

rust
pub struct StaticByOwnership { field: String}
pub struct ExplicitlyStatic { field: &'static str}

Errors in the context of Result

Surprisingly, the type wrapped by std::result::Result::Err doesn't need an Error bound:

rust
pub enum Result<T, E> { Ok(T), Err(E),}

You can use whatever type you want to represent an error inside Result.

The same is true for associated types in many trait definitions, such as std::str::FromStr:

rust
pub trait FromStr: Sized {
type Err;2
fn from_str(s: &str) -> Result<Self, Self::Err>;}

Err isn't bounded by Error 2!

Although you can use any types in these contexts, I strongly encourage you to only use Error implementations.

Other Rust developers will expect these things to behave like Errors, and we should strive to be as unsurprising as possible. That doesn't stop you from implementing additional functionality on your Errors, though.

There are exceptions to this rule, often within the standard library itself. Look out for the discussion of Error::downcast and Box<dyn Error> in the next section.

Okay, we've nailed the essentials. Let's get into the choice that confuses most new Rust developers: should we use dynamic or statically typed errors?

🦀🦀🦀

Part II

Dynamic error handling in Rust

When to use Box<dyn Error> and friends

Box<dyn Error> is Rust's vaguest error type. It's just some object that implements Error 🤷.

Box<dyn Error + Send + Sync + 'static> is its thread-safe counterpart.

The Error is boxed because, as a dynamic trait object, we don't know its size at compile time. We have to allocate it on the heap.

Box<dyn Error> simply says "something went wrong, check my message or my optional cause to know more".

This has two key properties:

  • It's excellent for quickly communicating that something went wrong.
  • It's god-awful at providing structured data for an error handler to act on.

If you would like consumers of your error – whether they're error handlers in your own application or users of your library – to be able to dynamically change their program's behavior based on the internal details of an error, don't use Box<dyn Error>.

Parsing error details from messages is fragile and hard to maintain. If you expect people to rely on your error messages to drive program behavior, you've also inadvertently made those error messages part of your public API. If that error message changes, code that parses it may break.

If you know that there's nothing useful a receiving program can do with the error, but that the message is helpful for a human debugger, then Box<dyn Error> and related trait objects are very convenient.

I work on an astrodynamics library for a space mission simulator funded by the European Space Agency. If someone inputs garbage data, like the time 23:59:60 on a year without leap seconds, there's really no way to recover. In this scenario, it would be perfectly reasonable to return Box<dyn Error> with a message that explains how silly they are.

Now, we don't actually do this – that's a story for Part III on structured errors – but it is a valid Rust error handling strategy.

Handling dynamic errors from other people's code

What if library code you call returns a dynamic error?

Hopefully, you just want to log it for a future debugging session. Surely the thoughtfully crafted error message will give you everything you need to solve the problem 🤡.

But say it doesn't, and you need to find out what's inside the dyn Error?

I don't envy you this situation. It's often an indicator of bad library design.

Moaning about it won't help you in the moment, though. You need to downcast.

Downcasting errors in Rust

Did you know that you can get a concrete error type back out of a boxed dyn Error?

I'm not going to get into how the std::error crate does this, because it involves some scary unsafe code that has nothing to do with handling errors. That won't stop us from using it.

dyn Error trait objects have three methods for attempting a transformation into some concrete type T:

rust
pub fn downcast<T: Error + 'static>(self: Box<Self>) -> Result<Box<T>, Box<Self>>pub fn downcast_mut<T: Error + 'static>(&mut self) -> Option<&mut T>pub fn downcast_ref<T: Error + 'static>(&self) -> Option<&T>

If the dyn Error is of type T, you'll get a T for closer inspection. Whether that T is owned or borrowed depends on which method you call.

All of this is useless if the underlying type is private to the crate the dyn Error came from. In this scenario, politely explain your predicament to the maintainers, then scream into a pillow.

Avoid forcing callers to downcast

I don't encourage designing your errors to require downcasting to figure out what's gone wrong.

If you choose to return a dynamic error, you are communicating that the internal structure of the error shouldn't matter to callers.

Forcing them to dig into your crate's error types, identify the possible culprits, downcast, and react dynamically screams "leaky implementation details".

This is Rust, not Go.

So what's the point of downcasting?

If downcasting isn't an ideal way to handle errors, what is it good for? Let's use Actix Web 4.7.0 as an example.

The primary Actix error struct, Error, has a single field, cause, that holds a Box<dyn ResponseError>.

rust
actix-web src/error/error.rs
pub struct Error { cause: Box<dyn ResponseError>,}

ResponseError is a trait with identical bounds to std::error::Error, but specifies methods to return a status code and an HTTP response body:

rust
actix-web src/error/response_error.rs
pub trait ResponseError: fmt::Debug + fmt::Display { fn status_code(&self) -> StatusCode fn error_response(&self) -> HttpResponse<BoxBody>}

It has default implementations for both of these methods, but they're not important here.

What is important is the large number of concrete error types that Actix provides ResponseError implementations for: Box<dyn std::error::Error + 'static>, Infallible, serde_json::Error, std::io::Error, and many more.

Naturally, Actix users can implement ResponseError for their own types too, so actix_web::error::Error chooses a dynamic error type to wrap a theoretically infinite variety of ResponseErrors.

Actix itself doesn't care about the internal structure of any particular ResponseError. It just needs a way to get a status code and response body when something goes wrong. This is a scenario where dynamic errors shine.

But you know who might care? The team whose code produced the error.

If an Actix user converts an error into Actix's opaque error format, they should reasonably expect to be able to get it out again. That's why actix_web::error::Error provides the as_error method, which downcasts to the user's original error type.

rust
actix-web src/error/error.rs
impl Error { pub fn as_error<T: ResponseError + 'static>(&self) -> Option<&T> { <dyn ResponseError>::downcast_ref(self.cause.as_ref()) }}

There are no leaky abstractions here, because the caller of as_error also owns the code that created the error in the first place.

Actix never calls downcast_ref itself. It doesn't use downcast_ref to handle errors. Rather, it provides as_error as a means for external parties using Actix's wrapper type to inspect their own implementation details.

Handling Rust errors with anyhow

What discussion of dynamic error handling in Rust would be complete without talking about anyhow?

anyhow is Rust's most-loved crate for handling errors in the laziest way possible.

anyhow::Error is effectively a Box<dyn Error + Send + Sync + 'static> with bells on. It always gives you a backtrace, and, unlike Box, it takes up only one machine word, not two (a "narrow pointer").

anyhow comes with a selection of macros, methods and blanket implementations to make wrapping and adding context to any Display + Send + Sync + 'static type a breeze.

Just like actix_web::error::Error, anyhow::Error is a wrapper for user-provided types. Seeing as those users might want their types back, it provides downcast methods in your three favorite flavors: owned, & and &mut.

I use anyhow often, and I find it's a better fit for applications than libraries.

If you return a concrete anyhow::Error across a crate boundary, you force the caller to depend directly on anyhow, and not everyone will want to.

As a general rule, return only your own or standard library error types across crate boundaries to minimize leakage of your implementation details into other people's code.

Who is your audience and what will they do with your error?

I hope it's becoming clear that how you choose to handle your errors depends on two key things:

  • Who the audience for the error is.
  • What they should be able to do with an error you give them.

Dynamic errors are great for consolidating a wide range of error types and returning them in a format where the only reasonable thing to do is write to output, whether that's a logger or an HTTP connection.

In Part III, we'll look at structured, statically typed errors as carriers of data that we can handle programmatically. More than that though, we'll see how they serve as invaluable, innate documentation for other developers.

When we understand both of these error handling styles, we'll bring them together, equipping ourselves with the knowledge to handle any kind of error that might arise, and avoid some nasty footguns.

🦀🦀🦀

Part III

Structured error handling in Rust

Part III is coming soon.

Share better Rust!

The Y Combinator logoThe Reddit logoThe X logo