Skip to content

Instantly share code, notes, and snippets.

@J-Cake
Last active March 26, 2024 14:10
Show Gist options
  • Save J-Cake/1f56b7f2ab78b86ceef791c1ffb9b3f0 to your computer and use it in GitHub Desktop.
Save J-Cake/1f56b7f2ab78b86ceef791c1ffb9b3f0 to your computer and use it in GitHub Desktop.
Rust Error macro

JCake's Error Macro

I like the ? syntax a lot because it is very readable and delegates errors nicely but it often becomes unwieldly because ? only coerces between like-typed error types. If you define a type which contains each error type and the necessary impls for automatic type coercion, your life becomes much easier. This macro does that. You define a list of all possible error types, and this macro spits out a system which you need only import.

Here a code example:

pub mod error;

pub use crate::error::*; // Include this line in each file you intend to use `error` in.

// Notice how by importing the `error` module, you can omit the `Error` generic type on the result
pub fn a_function_that_can_fail_in_numerous_ways() -> Result<()> {
  open_window()?;
  read_file()?;
  
  return Err(Error::from(ManualError::MiscellaneousError("Fail in some way that is known to you")))
}

Using and Defining error types

You will need to define a name for your error collection. I like global because I generally only have one per crate but you can pretty much define as many as you need. In the parentheses you can place names of various other error marker types. These can't be parameterised because I couldn't be fucked to implement that. Instead use the ManualError system. it's better anyway.

Each enumerable error type is given a name, followed by a type like so: Name = Error::Type;. For some other reason, I haven't managed to get trailing semicolons working, so you'll get compiler errors if you do that. So don't do that.

multi_error! { global()
  ManualError = crate::error::ManualError; // because the `global` name defines a module, and Rust somehow isn't smart enough to resolve the path here, you end up with confusing path errors, saying that `ManualError` doesn't exist, while `super::ManualError` does. So I just specify it absolutely.  
  IoError = std::io::Error
}

pub enum ManualError {
  ChildProcessDied { pid: usize },
  CryptographicHashFunctionProducedACollision(String),
}

Enjoy <3

macro_rules! multi_error {
($name:ident($($manual:ident),*); $($err:ident = $obj:ty);*) => {
pub mod $name {
use backtrace::Backtrace;
#[derive(Debug)]
pub enum Inner {
$($err($obj),)*
$($manual),*
}
impl std::fmt::Display for Inner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) } }
impl std::error::Error for Inner {}
$(impl From<$obj> for Inner { fn from(value: $obj) -> Self { Self::$err(value) } })*
pub struct Error {
inner: Inner,
backtrace: Backtrace
}
impl<Err> From<Err> for Error where Err: Into<Inner> {
fn from(err: Err) -> Self {
Self {
inner: err.into(),
backtrace: Backtrace::new()
}
}
}
impl std::error::Error for Error {}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { std::fmt::Debug::fmt(self, f) }
}
impl std::fmt::Debug for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}\n", &self.inner)?;
match std::env::var("RUST_BACKTRACE").as_ref().map(|i| i.as_ref()) {
Ok("full") => write!(f, "{:#?}", self.backtrace),
Ok("1") => write!(f, "{:?}", self.backtrace),
_ => write!(f, ""),
}
}
}
}
}
}
multi_error! { global();
ManualError = crate::error::ManualError;
IoError = std::io::Error
}
pub type Result<T> = ::std::result::Result<T, global::Error>;
pub use global::Error;
#[derive(Debug, Clone)]
pub enum ManualError {
}
impl std::error::Error for ManualError {}
impl std::fmt::Display for ManualError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment