Errors (part 1)
In Rust, errors are returned through the Result
type as we've seen in previous chapters. Errors are 'normal' structs.
Nevertheless, there is a convention to implement certain traits on types that represent an error:
the std::fmt::Display
and std::error:Error
traits from
the standard library.
A typical error implementation could look like this:
type SampleResult<T> = std::result::Result<T, SampleError>; #[derive(Debug, Clone)] struct SampleError { pub message: String, } impl SampleError { fn new(msg: &str) -> Self { SampleError { message: msg.to_string(), } } } impl std::fmt::Display for SampleError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "Sample error: {}", self.message) } } impl std::error::Error for SampleError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } } fn failable_function() -> SampleResult<u32> { Err(SampleError::new("oops")) } fn main() { if let Err(err) = failable_function() { println!("Error: {err}"); } }
It could be useful to convert (low-level) errors into your custom error type as you propagate errors up in the chain.
Rust has a "std::convert::From" trait
that facilitates this conversion.
You can use the
From
trait to write conversion code for all types of structs, not only errors.
use std::fs::read_to_string; use std::io; type SampleResult<T> = std::result::Result<T, SampleError>; #[derive(Debug, Clone)] struct SampleError { pub message: String, } impl std::fmt::Display for SampleError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "Sample error: {}", self.message) } } impl std::error::Error for SampleError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None } } impl std::convert::From<io::Error> for SampleError { fn from(io_err: io::Error) -> Self { SampleError { message: io_err.to_string(), } } } fn read_from_file() -> SampleResult<String> { match read_to_string("non_existing_file.txt") { Err(err) => Err(err.into()), Ok(content) => Ok(content), } } fn main() { if let Err(err) = read_from_file() { println!("Error: {err}"); } }
The
into()
function in the above code is the inverse of thefrom()
function. The compiler is clever enough to let you use both of these, whilst only implementing theFrom
trait. We could have also usedErr(SampleError::from(err))
.
The
match function() { Err(err) => ... , Ok(c) => ... }
pattern used in the above sample is a common way to check results. TheErr(err)
path can be used to quickly exit the function in case of an error.
Dealing with boilerplate
The above error handling code is quite verbose. There are popular crates that can help reduce the amount of boilerplate
code you need to write: anyhow
and thiserror
. These two crates follow a different approach to error handling, and
which one to pick depends on your use case.
anyhow
makes it extremely easy to write concise error handling code, but is less useful for capturing and dealing with
upstream errors. thiserror
is more verbose, but allows you to define your own error types and implement the Error
trait on them.
I'd suggest you use anyhow
when you are writing a client application, a CLI tool, or some other binary where you are
not exposing your error types to other libraries or applications. Use thiserror
when you are writing a library and
want
to define your own error types.
The anyhow
crate
The anyhow
crate is a popular choice for error handling in Rust. It allows you to write concise error handling code
without having to define your own error types. Here's how you can use it:
use anyhow::{anyhow, Result}; fn fallible_function() -> Result<u32> { Err(anyhow!("oops")) } fn main() -> Result<()> { if let Err(err) = fallible_function() { println!("Error: {err}"); } Ok(()) }
With the anyhow!
macro, you can create an error with a custom message. The Result
type is a type alias for
anyhow::Result<T>
, which is a Result
type that can hold any error type. This makes it easy to return errors from
functions without having to define your own error types.
The thiserror
crate
The thiserror
crate can help reduce the boilerplate code of creating custom error types. Here's how you can use it:
use thiserror::Error; #[derive(Error, Debug)] enum SampleError { #[error("An error occurred: {0}")] Custom(String), #[error("An IO error occurred: {0}")] Io(#[from] std::io::Error), #[error("Some other error occurred: {0}")] Other(String), } fn fallible_function() -> Result<u32, SampleError> { Err(SampleError::Custom("oops".to_string())) } fn fallible_io_operation() -> Result<u32, SampleError> { let _ = std::fs::File::open("non_existent_file.txt")?; Ok(42) } fn main() -> Result<(), SampleError> { if let Err(err) = fallible_function() { match err { SampleError::Custom(msg) => println!("Custom error: {}", msg), SampleError::Io(err) => println!("IO error: {}", err), SampleError::Other(msg) => println!("Other error: {}", msg), } } fallible_io_operation()?; Ok(()) }
As you can see, the thiserror
crate allows you to define your own error types and implement the Error
trait on them
with minimal boilerplate code. It allows "source" errors to be converted into your custom error type, and provides a
convenient way to match on different error variants.
We'll revisit the error topic in a bit. First, I'd like to cover the structure of a Rust project.