But there seems to be something missing: error handling. After using Rust on and off for a couple of years, a thing I still don’t understand is how to write idiomatic code which deals with multiple different error types.
But there seems to be something missing: error handling. After using Rust on and off for a couple of years, a thing I still don’t understand is how to write idiomatic code which deals with multiple different error types.
If this is not a library then feel free to use any number of error handling helper libraries out there. It would be unfair to single one of them out. However, once I got the hang of writing my on custom errors I ended up using them for my binary applications too.
This is exactly the path that my SNAFU library takes. It’s a procedural macro attached to an enum that generates the From, Display and Error traits for you.
This way, consumers of your code don’t need to know anything beyond the fact that you implement Error and any inherent public methods or traits you choose to expose. The implementation provided by SNAFU is internal to your code.
Library: thiserror
Application: anyhow (or eyre if you want the same thing but with fancy colors and nicer backtraces).
Make sure you #[from] to derive From a lot. The value is in chains. Here's an example I just wrote
#[derive(thiserror::Error)]
enum LoadError {
#[error("Failed to parse")]
Parse(#[from] ParseError),
#[error("Failed to acquire lock")]
Lock(#[from] file_lock::AcquireError),
#[error("IO error")
Io(#[from] std::io::Error),
}
ParseError and AcquireError are defined in a similar style.Then I have a function
fn load(...) -> Result<..., LoadError> {
let lock = FileLock::acquire(...)?;
let parsed = parse(...)?;
Ok(parsed)
}
The magic is that ? calls .into on the error, which uses a From implemention to convert the error into the error type the function returns. This means the ParseError gets converted into a LoadError::Parse, for example.Now we've got that nice semantic cause chain, we use anyhow to display it.
fn main() -> anyhow:: Result<()> {
let index = load(...)?;
// ...
Ok(())
}
If index fails I'll get something like this printed: Error: Failed to acquire lock
Caused by:
Failed to open lock
No such file or directory (OS error 2)
It feels like a superpower to have a high-level function fail and see the massive list of errors all the way down to the root cause.I am not sure if it is idiomatic but I have been following the guidelines int this article for general error handling:
https://nick.groenen.me/posts/rust-error-handling/
And this article for having context: