Item 4: Prefer idiomatic Error types

Item 3 described how to use the transformations that the standard library provides for the Option and Result types to allow concise, idiomatic handling of result types using the ? operator. It stopped short of discussing how best to handle the variety of different error types E that arise as the second type argument of a Result<T, E>; that's the subject of this Item.

This is relevant only when there are a variety of different error types in play. If all of the different errors that a function encounters are already of the same type, it can just return that type. When there are errors of different types, there's a decision to make about whether the suberror type information should be preserved.

The Error Trait

It's always good to understand what the standard traits (Item 10) involve, and the relevant trait here is std::error::Error. The E type parameter for a Result doesn't have to be a type that implements Error, but it's a common convention that allows wrappers to express appropriate trait bounds—so prefer to implement Error for your error types.

The first thing to notice is that the only hard requirement for Error types is the trait bounds: any type that implements Error also has to implement the following traits:

  • The Display trait, meaning that it can be format!ed with {}
  • The Debug trait, meaning that it can be format!ed with {:?}

In other words, it should be possible to display Error types to both the user and the programmer.

The only method in the trait is source(),1 which allows an Error type to expose an inner, nested error. This method is optional—it comes with a default implementation (Item 13) returning None, indicating that inner error information isn't available.

One final thing to note: if you're writing code for a no_std environment (Item 33), it may not be possible to implement Error—the Error trait is currently implemented in std, not core, and so is not available.2

Minimal Errors

If nested error information isn't needed, then an implementation of the Error type need not be much more than a String—one rare occasion where a "stringly typed" variable might be appropriate. It does need to be a little more than a String though; while it's possible to use String as the E type parameter:

pub fn find_user(username: &str) -> Result<UserId, String> {
    let f = std::fs::File::open("/etc/passwd")
        .map_err(|e| format!("Failed to open password file: {:?}", e))?;
    // ...
}

a String doesn't implement Error, which we'd prefer so that other areas of code can deal with Errors. It's not possible to impl Error for String, because neither the trait nor the type belong to us (the so-called orphan rule):

impl std::error::Error for String {}
error[E0117]: only traits defined in the current crate can be implemented for
              types defined outside of the crate
  --> src/main.rs:18:5
   |
18 |     impl std::error::Error for String {}
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^------
   |     |                          |
   |     |                          `String` is not defined in the current crate
   |     impl doesn't use only types from inside the current crate
   |
   = note: define and implement a trait or new type instead

A type alias doesn't help either, because it doesn't create a new type and so doesn't change the error message:

pub type MyError = String;

impl std::error::Error for MyError {}
error[E0117]: only traits defined in the current crate can be implemented for
              types defined outside of the crate
  --> src/main.rs:41:5
   |
41 |     impl std::error::Error for MyError {}
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^-------
   |     |                          |
   |     |                          `String` is not defined in the current crate
   |     impl doesn't use only types from inside the current crate
   |
   = note: define and implement a trait or new type instead

As usual, the compiler error message gives a hint to solving the problem. Defining a tuple struct that wraps the String type (the "newtype pattern", Item 6) allows the Error trait to be implemented, provided that Debug and Display are implemented too:

#[derive(Debug)]
pub struct MyError(String);

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl std::error::Error for MyError {}

pub fn find_user(username: &str) -> Result<UserId, MyError> {
    let f = std::fs::File::open("/etc/passwd").map_err(|e| {
        MyError(format!("Failed to open password file: {:?}", e))
    })?;
    // ...
}

For convenience, it may make sense to implement the From<String> trait to allow string values to be easily converted into MyError instances (Item 5):

impl From<String> for MyError {
    fn from(msg: String) -> Self {
        Self(msg)
    }
}

When it encounters the question mark operator (?), the compiler will automatically apply any relevant From trait implementations that are needed to reach the destination error return type. This allows further minimization:

pub fn find_user(username: &str) -> Result<UserId, MyError> {
    let f = std::fs::File::open("/etc/passwd")
        .map_err(|e| format!("Failed to open password file: {:?}", e))?;
    // ...
}

The error path here covers the following steps:

  • File::open returns an error of type std::io::Error.
  • format! converts this to a String, using the Debug implementation of std::io::Error.
  • ? makes the compiler look for and use a From implementation that can take it from String to MyError.

Nested Errors

The alternative scenario is where the content of nested errors is important enough that it should be preserved and made available to the caller.

Consider a library function that attempts to return the first line of a file as a string, as long as the line is not too long. A moment's thought reveals (at least) three distinct types of failure that could occur:

  • The file might not exist or might be inaccessible for reading.
  • The file might contain data that isn't valid UTF-8 and so can't be converted into a String.
  • The file might have a first line that is too long.

In line with Item 1, you can use the type system to express and encompass all of these possibilities as an enum:

#![allow(unused)]
fn main() {
#[derive(Debug)]
pub enum MyError {
    Io(std::io::Error),
    Utf8(std::string::FromUtf8Error),
    General(String),
}
}

This enum definition includes a derive(Debug), but to satisfy the Error trait, a Display implementation is also needed:

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MyError::Io(e) => write!(f, "IO error: {}", e),
            MyError::Utf8(e) => write!(f, "UTF-8 error: {}", e),
            MyError::General(s) => write!(f, "General error: {}", s),
        }
    }
}

It also makes sense to override the default source() implementation for easy access to nested errors:

use std::error::Error;

impl Error for MyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            MyError::Io(e) => Some(e),
            MyError::Utf8(e) => Some(e),
            MyError::General(_) => None,
        }
    }
}

The use of an enum allows the error handling to be concise while still preserving all of the type information across different classes of error:

use std::io::BufRead; // for `.read_until()`

/// Maximum supported line length.
const MAX_LEN: usize = 1024;

/// Return the first line of the given file.
pub fn first_line(filename: &str) -> Result<String, MyError> {
    let file = std::fs::File::open(filename).map_err(MyError::Io)?;
    let mut reader = std::io::BufReader::new(file);

    // (A real implementation could just use `reader.read_line()`)
    let mut buf = vec![];
    let len = reader.read_until(b'\n', &mut buf).map_err(MyError::Io)?;
    let result = String::from_utf8(buf).map_err(MyError::Utf8)?;
    if result.len() > MAX_LEN {
        return Err(MyError::General(format!("Line too long: {}", len)));
    }
    Ok(result)
}

It's also a good idea to implement the From trait for all of the suberror types (Item 5):

impl From<std::io::Error> for MyError {
    fn from(e: std::io::Error) -> Self {
        Self::Io(e)
    }
}
impl From<std::string::FromUtf8Error> for MyError {
    fn from(e: std::string::FromUtf8Error) -> Self {
        Self::Utf8(e)
    }
}

This prevents library users from suffering under the orphan rules themselves: they aren't allowed to implement From on MyError, because both the trait and the struct are external to them.

Better still, implementing From allows for even more concision, because the question mark operator will automatically perform any necessary From conversions, removing the need for .map_err():

use std::io::BufRead; // for `.read_until()`

/// Maximum supported line length.
pub const MAX_LEN: usize = 1024;

/// Return the first line of the given file.
pub fn first_line(filename: &str) -> Result<String, MyError> {
    let file = std::fs::File::open(filename)?; // `From<std::io::Error>`
    let mut reader = std::io::BufReader::new(file);
    let mut buf = vec![];
    let len = reader.read_until(b'\n', &mut buf)?; // `From<std::io::Error>`
    let result = String::from_utf8(buf)?; // `From<string::FromUtf8Error>`
    if result.len() > MAX_LEN {
        return Err(MyError::General(format!("Line too long: {}", len)));
    }
    Ok(result)
}

Writing a complete error type can involve a fair amount of boilerplate, which makes it a good candidate for automation via a derive macro (Item 28). However, there's no need to write such a macro yourself: consider using the thiserror crate from David Tolnay, which provides a high-quality, widely used implementation of just such a macro. The code generated by thiserror is also careful to avoid making any thiserror types visible in the generated API, which in turn means that the concerns associated with Item 24 don't apply.

Trait Objects

The first approach to nested errors threw away all of the suberror detail, just preserving some string output (format!("{:?}", err)). The second approach preserved the full type information for all possible suberrors but required a full enumeration of all possible types of suberror.

This raises the question, Is there a middle ground between these two approaches, preserving suberror information without needing to manually include every possible error type?

Encoding the suberror information as a trait object avoids the need for an enum variant for every possibility but erases the details of the specific underlying error types. The receiver of such an object would have access to the methods of the Error trait and its trait bounds—source(), Display::fmt(), and Debug::fmt(), in turn—but wouldn't know the original static type of the suberror:

#[derive(Debug)]
pub enum WrappedError {
    Wrapped(Box<dyn Error>),
    General(String),
}

impl std::fmt::Display for WrappedError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Wrapped(e) => write!(f, "Inner error: {}", e),
            Self::General(s) => write!(f, "{}", s),
        }
    }
}

It turns out that this is possible, but it's surprisingly subtle. Part of the difficulty comes from the object safety constraints on trait objects (Item 12), but Rust's coherence rules also come into play, which (roughly) say that there can be at most one implementation of a trait for a type.

A putative WrappedError type would naively be expected to implement both of the following:

  • The Error trait, because it is an error itself.
  • The From<Error> trait, to allow suberrors to be easily wrapped.

That means that a WrappedError can be created from an inner WrappedError, as WrappedError implements Error, and that clashes with the blanket reflexive implementation of From:

impl Error for WrappedError {}

impl<E: 'static + Error> From<E> for WrappedError {
    fn from(e: E) -> Self {
        Self::Wrapped(Box::new(e))
    }
}
error[E0119]: conflicting implementations of trait `From<WrappedError>` for
              type `WrappedError`
   --> src/main.rs:279:5
    |
279 |     impl<E: 'static + Error> From<E> for WrappedError {
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
    = note: conflicting implementation in crate `core`:
            - impl<T> From<T> for T;

David Tolnay's anyhow is a crate that has already solved these problems (by adding an extra level of indirection via Box) and that adds other helpful features (such as stack traces) besides. As a result, it is rapidly becoming the standard recommendation for error handling—a recommendation seconded here: consider using the anyhow crate for error handling in applications.

Libraries Versus Applications

The final advice from the previous section included the qualification "…for error handling in applications". That's because there's often a distinction between code that's written for reuse in a library and code that forms a top-level application.3

Code that's written for a library can't predict the environment in which the code is used, so it's preferable to emit concrete, detailed error information and leave the caller to figure out how to use that information. This leans toward the enum-style nested errors described previously (and also avoids a dependency on anyhow in the public API of the library, see Item 24).

However, application code typically needs to concentrate more on how to present errors to the user. It also potentially has to cope with all of the different error types emitted by all of the libraries that are present in its dependency graph (Item 25). As such, a more dynamic error type (such as anyhow::Error) makes error handling simpler and more consistent across the application.

Things to Remember

  • The standard Error trait requires little of you, so prefer to implement it for your error types.
  • When dealing with heterogeneous underlying error types, decide whether it's necessary to preserve those types.
    • If not, consider using anyhow to wrap suberrors in application code.
    • If so, encode them in an enum and provide conversions. Consider using thiserror to help with this.
  • Consider using the anyhow crate for convenient idiomatic error handling in application code.
  • It's your decision, but whatever you decide, encode it in the type system (Item 1).

1

Or at least the only nondeprecated, stable method.

2

At the time of writing, Error has been moved to core but is not yet available in stable Rust.