| theme | gaia |
|---|---|
| paginate | true |
the young Rust community was debating on how to do error handling in the language.
// Rust 1.0 from 2015:
pub trait Error: Debug + Display {
fn description(&self) -> &str
fn cause(&self) -> Option<&dyn Error>
}(the dyn was actually not part of the signature back in the day)
description: Get a human-readable error messagecause: Get the original error (if any)
2018: RFC 2504 Fix the error trait
- Rust 1.30.0: Deprecate
causeand introducefn source(&self) -> Option<&(dyn std::error::Error + 'static)> - Rust 1.42.0: Deprecate
descriptionin favor of thestd::fmt::Displaytrait which gives us ownedStrings for error messages. - (also some excluded ideas about backtraces)
// we ignore the deprecated and unstable methods
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
}std::fmt::Debugcan bederived for your typesstd::fmt::Displayis annoying to implementsource()is also tricky to implement due to lifetimes and static typing.
impl fmt::Display for MyType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "imagine some meaningful user-facing text here")
}
}The text that is shown to the user is typically the only thing that differs between different implementations. The rest is just boilerplate.
- People wanted to make implementing errors easier
- Many different error handling libraries were created
- And based on the experiences with those libraries, the official
std::error::Errortrait was also improved - But the need for error libraries still exists, because the standard library prefers to only offer basic functionality to stay flexible
Both can be used to quickly create error types
thiserror: Typically used in library codeanyhow: Typically used in application code- The code produced by
thiserrordoes not appear in your public API. It's only there to reduce boilerplate.
In your Cargo.toml
[dependencies]
thiserror = "1.0"In your code:
use thiserror::Error as ThisError;
// your error types hereuse thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
#[error("bad things happened: {message}")]
pub struct MyError {
message: String,
}use thiserror::Error as ThisError;
const MIN_TEMP: usize = 10;
#[derive(Debug, ThisError)]
pub enum MyEnumError {
#[error("things simply didn't work")]
SimpleVariant,
#[error("found only {0} entries")]
NotEnoughEntries(usize),
#[error("Not warm enough: {temperature} {unit} < {}", MIN_TEMP)]
Temperature { temperature: usize, unit: String },
}fn foo() -> Result<(), FooError> {
Ok(bar()?)
}
fn bar() -> Result<(), BarError> {
Ok(baz()?)
}
fn baz() -> Result<(), BazError> {
Err(BazError{})
}call chain: foo() -> bar() -> baz()
error chain: BazError <- BarError <- FooError
There are two options:
- Use
#[from]if you have an error type or variant that contains only the inner error (and possibly a backtrace).#[from]generates automatic?operator conversion code for you. - Use
#[source]if you need to store more data in the error. You need to write the conversion code yourself, becausethiserrorcannot guess how to fill the extra values in your error type.
use thiserror::Error as ThisError;
#[derive(ThisError, Debug)]
#[error("something on the inside went wrong")]
pub struct MyInnerError {} // empty error type for demonstration purposes
#[derive(ThisError, Debug)]
pub enum MyError {
#[error("things broke on the inside")]
InnerProblem {
#[from] inner: MyInnerError,
},
#[error("IO troubles")]
IO(#[from] std::io::Error),
}use thiserror::Error as ThisError;
#[derive(ThisError, Debug)]
#[error("Terrible things happened: {message}")]
pub struct MyError {
#[source] // optional if field name is `source`
source: MyInnerError, // if we know it is always this error type
message: String, // we can't use #[from] because of this extra field
}flexible option: source: Option<Box<dyn std::error::Error>>
flexible easy option: source: anyhow::Error (from the anyhow crate)
You can forward the source and std::fmt::Display implementations from the inner error to the outer error.
#[derive(Debug, ThisError)]
pub enum MyError {
#[error("IO failed")] // we provide the error message ourselves
IO(#[from] std::io::Error),
#[error(transparent)] // use source and Display from inner error
Other(#[from] anyhow::Error), // catch-all for any other inner error type
}- Error handling in Rust can require writing a lot of boilerplate code, because the standard library only provides basic functionality
- Error libraries like
thiserrorcan reduce the boilerplate thiserroruses the#[error(text)]macro to generatestd::fmt::Displayimplementations with format strings.thiserrorcan generatestd::convert::From<SomeInnerError>implementations when you use the#[from]macro.thiserrorcan generate thestd::error::Error::sourcemethod for you, when you use the#[source]macro or when you name the inner error fieldsource(or when you use#[from])
