Custom Error Types

Rust allow us to create our own Err types. We call them “Custom Error Types”.

Error trait

As you know traits define the functionality a type must provide. But we don’t always need to define new traits for common functionalities, because Rust standard library provides reusable traits which can be implemented on our own types. While creating custom error types the std::error::Error trait helps us to convert any type to an Err type.

use std::fmt::{Debug, Display};

pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(Error + 'static)> { ... }
}

As we discussed under traits inheritance, a trait can be inherited from another traits. trait Error: Debug + Display means Error trait inherits from fmt::Debug and fmt::Display traits.

// traits inside Rust standard library core fmt module/ std::fmt
pub trait Display {
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
}

pub trait Debug {
    fn fmt(&self, f: &mut Formatter) -> Result<(), Error>;
}
  • Display

    • How should the end user see this error as a message/ user-facing output.
    • Usually print via println!("{}") or eprintln!("{}")
  • Debug

    • How should display the Err while debugging/ programmer-facing output.
    • Usually print via println!("{:?}") or eprintln!("{:?}")
    • To pretty-print, println!("{:#?}") or eprintln!("{:#?}") can be used.
  • source()

    • The lower-level source of this error, if any.
    • Optional.

First, let’s see how to implement std::error::Error trait on a simplest custom error type.

use std::fmt;

// Custom error type; can be any type which defined in the current crate
// 💡 In here, we use a simple "unit struct" to simplify the example
struct AppError;

// Implement std::fmt::Display for AppError
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "An Error Occurred, Please Try Again!") // user-facing output
    }
}

// Implement std::fmt::Debug for AppError
impl fmt::Debug for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{{ file: {}, line: {} }}", file!(), line!()) // programmer-facing output
    }
}

// A sample function to produce an AppError Err
fn produce_error() -> Result<(), AppError> {
    Err(AppError)
}

fn main() {
    match produce_error() {
        Err(e) => eprintln!("{}", e), // An Error Occurred, Please Try Again!
        _ => println!("No error"),
    }

    eprintln!("{:?}", produce_error()); // Err({ file: src/main.rs, line: 17 })
}

Hope you understood the main points. Now, let’s see a custom error type with an error code and an error message.

use std::fmt;

struct AppError {
    code: usize,
    message: String,
}

// Different error messages according to AppError.code
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let err_msg = match self.code {
            404 => "Sorry, Can not find the Page!",
            _ => "Sorry, something is wrong! Please Try Again!",
        };

        write!(f, "{}", err_msg)
    }
}

// A unique format for dubugging output
impl fmt::Debug for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "AppError {{ code: {}, message: {} }}",
            self.code, self.message
        )
    }
}

fn produce_error() -> Result<(), AppError> {
    Err(AppError {
        code: 404,
        message: String::from("Page not found"),
    })
}

fn main() {
    match produce_error() {
        Err(e) => eprintln!("{}", e), // Sorry, Can not find the Page!
        _ => println!("No error"),
    }

    eprintln!("{:?}", produce_error()); // Err(AppError { code: 404, message: Page not found })

    eprintln!("{:#?}", produce_error());
    // Err(
    //     AppError { code: 404, message: Page not found }
    // )
}

⭐️ Rust standard library provides not only reusable traits and also it facilitates to magically generate implementations for few traits via #[derive] attribute. Rust support derive std::fmt::Debug, to provide a default format for debug messages. So we can skip std::fmt::Debug implementation for custom error types and use #[derive(Debug)] before struct declaration.

For a struct #[derive(Debug)] prints, the name of the struct , { , comma-separated list of each field’s name and debug value and }.

use std::fmt;

#[derive(Debug)] // derive std::fmt::Debug on AppError
struct AppError {
    code: usize,
    message: String,
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let err_msg = match self.code {
            404 => "Sorry, Can not find the Page!",
            _ => "Sorry, something is wrong! Please Try Again!",
        };

        write!(f, "{}", err_msg)
    }
}

fn produce_error() -> Result<(), AppError> {
    Err(AppError {
        code: 404,
        message: String::from("Page not found"),
    })
}

fn main() {
    match produce_error() {
        Err(e) => eprintln!("{}", e), // Sorry, Can not find the Page!
        _ => println!("No error"),
    }

    eprintln!("{:?}", produce_error()); // Err(AppError { code: 404, message: Page not found })

    eprintln!("{:#?}", produce_error());
    // Err(
    //     AppError {
    //         code: 404,
    //         message: "Page not found"
    //     }
    // )
}

From trait

When writing real programs, we mostly have to deal with different modules, different std and third party crates at the same time. Each crate uses their own error types. However, if we are using our own error type, we should convert those errors into our error type. For these conversions, we can use the standardized trait std::convert::From.

// traits inside Rust standard library core convert module/ std::convert
pub trait From<T>: Sized {
  fn from(_: T) -> Self;
}

💡 As you know, String::from() function is used to create a String from &str data type. Actually this also an implementation of std::convert::From trait.

Let’s see how to implement std::convert::From trait on a custom error type.

use std::fs::File;
use std::io;

#[derive(Debug)]
struct AppError {
    kind: String,    // type of the error
    message: String, // error message
}

// Implement std::convert::From for AppError; from io::Error
impl From<io::Error> for AppError {
    fn from(error: io::Error) -> Self {
        AppError {
            kind: String::from("io"),
            message: error.to_string(),
        }
    }
}

fn main() -> Result<(), AppError> {
    let _file = File::open("nonexistent_file.txt")?; // This generates an io::Error. But because of return type is Result<(), AppError>, it converts to AppError

    Ok(())
}


// --------------- Run time error ---------------
Error: AppError { kind: "io", message: "No such file or directory (os error 2)" }

In the above example, File::open(“nonexistent.txt”)? produces std::io::Error. But because of the return type is Result<(), AppError>, it converts to an AppError. Because of we are propagating the error from main() function, it prints the Debug representation of the Err.

In the above example we deal with only one std error type, std::io::Error. Let’s see some example which handles multiple std error types.

use std::fs::File;
use std::io::{self, Read};
use std::num;

#[derive(Debug)]
struct AppError {
    kind: String,
    message: String,
}

// Implement std::convert::From for AppError; from io::Error
impl From<io::Error> for AppError {
    fn from(error: io::Error) -> Self {
        AppError {
            kind: String::from("io"),
            message: error.to_string(),
        }
    }
}

// Implement std::convert::From for AppError; from num::ParseIntError
impl From<num::ParseIntError> for AppError {
    fn from(error: num::ParseIntError) -> Self {
        AppError {
            kind: String::from("parse"),
            message: error.to_string(),
        }
    }
}

fn main() -> Result<(), AppError> {
    let mut file = File::open("hello_world.txt")?; // generates an io::Error, if can not open the file and converts to an AppError

    let mut content = String::new();
    file.read_to_string(&mut content)?; // generates an io::Error, if can not read file content and converts to an AppError

    let _number: usize;
    _number = content.parse()?; // generates num::ParseIntError, if can not convert file content to usize and converts to an AppError

    Ok(())
}


// --------------- Few possible run time errors ---------------

// 01. If hello_world.txt is a nonexistent file
Error: AppError { kind: "io", message: "No such file or directory (os error 2)" }

// 02. If user doesn't have relevant permission to access hello_world.txt
Error: AppError { kind: "io", message: "Permission denied (os error 13)" }

// 03. If hello_world.txt contains non-numeric content. ex Hello, world!
Error: AppError { kind: "parse", message: "invalid digit found in string" }

🔎 Search about the implementation of std::io::ErrorKind, to see how to organize error types further.