Raw
Functions may fail in more than one way and it's useful to communicate the reason your program failed.
Errors are commonly managed through error enumeration because they allow easy definition of the errors and you can match over the error enum to handle specific error conditions.
Error type Requirements
Implement the Debug
trait - allows to display of the error information in debug contexts
#[derive(Debug)]
enum AuthError {
NotAuthenticated,
NotAuthorized,
}
Implement the Display
trait - allows to display of the error in user contexts. An example can be found in the docs.
use std::fmt;
impl fmt::Display for AuthError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::NotAuthenticated => write!(f, "You're not authenticated")
Self::NotAuthorized => write!(f, "You're not authorized")
}
}
}
Implement the Error
trait - allows interoperability with other errors. This can be done with an empty implementation block because it has a default implementation.
use std::error::Error;
impl Error for AuthError {} // use the default Error implementation
Error implementation can be automated using the thiserror
create. After adding it to the Cargo.toml
as a dependency
use thiserror::Error;
enum AuthorizationError {
#[error("Insufficient permissions to access the resource")]
InsufficientPermission,
#[error("You're banned from using this feature")]
Banned,
}
#[derive(Debug, Error)]
enum AuthError {
#[error("User does not exist")]
NotFound(i32), // returns a code of some sort
#[error("You're not authenticated")]
NotAuthenticated,
#[error("You're not authorized")]
NotAuthorized(#[from] AuthorizationError), // when a NotAuthorized
// error ocures - it will be transform an AuthorizationError into AuthError automatically
}
Using Custom Error Types
fn login() -> Result<(), AuthError> {
// something bad happened
Err(AuthError::NotAuthorized(5482))
}
The DOs for Custom Errors in Rust
Prefer to user error enums over strings
More Concisely communicated the problem
Can be used with
match
Keep the errors specific
Single modules specific for common module errors
Single functions specific for function-specific edge cases
Try to use
match
as much as possible
The DONTs for Custom Errors in Rust
Don't put unrelated errors into a single error enum
As the problem domain expands, the enumeration will become unwieldly
Changes to the enumeration will cascade across the entire codebase
Unclear which errors can be generated by a function
Example
use chrono::{DateTime, Duration, Utc};
use thiserror::Error;
struct SubwayPass {
id: usize,
funds: isize,
expires: DateTime<Utc>,
}
#[derive(Debug, Error)]
enum PassError {
#[error("Pass has expired, please renew it")]
PassExpired,
#[error("You have insufficient funds: {0}\nTop up now!")]
InsufficientFunds(isize),
#[error("Failed to read the pass {0}, try again")]
ReadError(String),
}
fn swipe_card() -> Result<SubwayPass, PassError> {
Ok(SubwayPass {
id: 0,
funds: 0,
expires: Utc::now() + Duration::weeks(30),
})
}
fn use_pass(pass: &mut SubwayPass, cost: isize) -> Result<(), PassError> {
if Utc::now() > pass.expires {
Err(PassError::PassExpired)
} else {
if pass.funds - cost < 0 {
Err(PassError::InsufficientFunds(pass.funds))
} else {
pass.funds = pass.funds - cost;
Ok(())
}
}
}
fn main() {
let pass_status = swipe_card().and_then(|mut pass| use_pass(&mut pass, 3)); // and_then - do something if Ok is returned
match pass_status {
Ok(_) => println!("ok to board"),
Err(e) => match e {
PassError::PassExpired => (), // trigger renewal flow
PassError::InsufficientFunds(_f) => (), // trigger top up flow
PassError::ReadError(_s) => (), // error screen
},
}
}