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 beformat!
ed with{}
- The
Debug
trait, meaning that it can beformat!
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 Error
s. 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 typestd::io::Error
.format!
converts this to aString
, using theDebug
implementation ofstd::io::Error
.?
makes the compiler look for and use aFrom
implementation that can take it fromString
toMyError
.
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 usingthiserror
to help with this.
- If not, consider using
- 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).
Or at least the only nondeprecated, stable method.
At the time of writing, Error
has been moved to core
but is not yet available in stable Rust.
This section is inspired by Nick Groenen's "Rust: Structuring and Handling Errors in 2020" article.