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.

- Test Code - For tests, examples, and early code, we want something loose, simple, and mostly transparent. If context information is needed,
static str
or a formattedString
is sufficient. - Production Code - For production code (app or lib), the benefits of starting with a more strict approach and taking full control typically yield more benefits as the code grows.
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 thanthiserror
as it allows us to selectively use the convenientFrom
derive proc macro and/orDisplay
if we need to display.