Rust: Custom Error Types

·

3 min read

Play this article

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

  1. Prefer to user error enums over strings

    • More Concisely communicated the problem

    • Can be used with match

💡
Use strings during prototyping or when the problem domain isn't fully understood
  1. Keep the errors specific

    • Single modules specific for common module errors

    • Single functions specific for function-specific edge cases

  2. Try to use match as much as possible

The DONTs for Custom Errors in Rust

  1. 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
        },
    }
}