Error Handling

Rust Programming Best Practices

// -- For tests, examples, early dev
pub type Result<T> = core::result::Result<T, Error>;
pub type Error = Box<dyn std::error::Error>; // For early dev.

// -- For lib, main prod code
use derive_more::From;

pub type Result<T> = core::result::Result<T, Error>;

#[derive(Debug, From)]
pub enum Error {
	// -- fs
	LimitToHigh { actual: usize, max: usize },

	// -- Externals
	#[from]
	Io(std::io::Error), // as example
}

Here are some best practices for error handling that can work relatively well for small to large codebases.

Approach

There are two sides of the spectrum for error handling.

Error Handling

The important aspect of these best practices is to utilize the same ? technique, i.e., (..)? on both sides of the spectrum, and avoid unwrap(), expect(), and even context(..) from anyhow.

For Test Code

For tests, examples, and early code, using the Box Dyn Error pattern actually gives a lot of flexibility and comes very close to the anyhow trait, but without the custom API.

So, one approach is to define the type aliases as below and use them in the appropriate method function.

pub type Result<T> = core::result::Result<T, Error>;
pub type Error = Box<dyn std::error::Error>; // For early dev.

fn hello_world() -> Result<String> {
    Ok(format!("Hello, World!"))
}

fn main() -> Result<()> {
    let message = hello_world()?;
    println!("{message}");

    // Will work because std::fs::io::Error will convert into Box Dyn Error.
    let contents = std::fs::read_to_string("src/main.rs")?;

    Ok(())
}

In fact, to prep the code for production, we can even put both of those type aliases in their error.rs

error.rs

pub type Result<T> = core::result::Result<T, Error>;
pub type Error = Box<dyn std::error::Error>; // For early dev.

And then, re-export them as we would do for concrete Error types in our parent module (in early dev, that would be main.rs or lib.rs, but could be modules as well)

mod error;

pub use self::error::{Error, Result};

For Production Code

Then for production code, we keep the type alias for Result<T> but can fully define the error type using an Enum type.

use derive_more::From;

pub type Result<T> = core::result::Result<T, Error>;

#[derive(Debug, From)]
pub enum Error {
    // -- fs
    LimitTooHigh { actual: usize, max: usize },

    // -- Externals
    #[from]
    Io(std::io::Error), // as an example
}

// Note: Implement Display as debug, for Web and app error, as anyway those errors will need to be streamed as JSON probably 
//       to be rendered for the end user. 
impl core::fmt::Display for Error {
    fn fmt(&self, fmt: &mut core::fmt::Formatter) -> core::result::Result<(), core::fmt::Error> {
        write!(fmt, "{self:?}")
    }
}

impl std::error::Error for Error {}

Note: Here we use derive_more rather than thiserror as it allows us to selectively use the convenient From derive proc macro and/or Display if we need to display.