◀ prevnext ▶

An even better Error Type

Programming · Feb 11th, 2024

My last post caused quite a stir in the Rust subreddit lol. Luckily, it's not a hill I am willing to die on. The post was more about having fun, poking holes on some annoyance I face while programming in Rust every day, than truly throwing hate to something or someone. I mean, who would plaster memes on a subject they are 100% serious about, right?

Nonetheless, I did get some value out of it. A single commenter pointed out that actually, Rust does have a utility to capture the stack trace. Well, it isn't built into Result, and you still need to call the function manually, but it exists. And poof. My biggest argument against Result just evaporated.

Capturing the stack trace allows for some beautiful ergonomics: Calling file!() and line!() isn't as important anymore, as all information is on the stack trace anyway. This means, no macro for conversion between errors is necessary, and now it actually makes sense to implement a generic From, that converts every std::error::Error into a RisError. Sure, it adds one more frame to the stack, but who cares? The entire thing is captured anyway ¯\_(ツ)_/¯

I find it quite silly that so many people recommended to just use some crate. Especially since the entire implementation, plus some utility, fits into a little over 100 lines of code. Again, it was just a single person who realized the actual problem. I am really getting is-odd vibes from this situation.

Enough condescending talk about redditors. Here's the new, better RisError:

GitHub permalink

use std::backtrace::Backtrace;
use std::error::Error;
use std::sync::Arc;

pub type SourceError = Option<Arc<dyn Error + 'static>>;
pub type RisResult<T> = Result<T, RisError>;

#[derive(Clone)]
pub struct RisError {
    pub source: SourceError,
    pub message: Option<String>,
    pub file: String,
    pub line: u32,
    pub backtrace: Arc<Backtrace>,
}

impl std::fmt::Display for RisError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(source) = &self.source {
            write!(f, "{}", source)?;
        }
        
        if let Some(message) = &self.message {
            write!(f, "\n    message: \"{}\"", message)?;
        }
        
        write!(f, "\n    at {}:{}", self.file, self.line)
    }
}

impl std::fmt::Debug for RisError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}\nbacktrace:\n{}", self, self.backtrace)
    }
}

#[derive(Debug)]
pub struct OptionError;

impl Error for OptionError {}

impl std::fmt::Display for OptionError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Option was None")
    }
}

impl<E: Error + 'static> From<E> for RisError {
    fn from(value: E) -> Self {
        Self {
            source: Some(Arc::new(value)),
            message: None,
            file: String::from(file!()),
            line: line!(),
            backtrace: crate::get_backtrace!(),
        }
    }
}

pub trait Extensions<T> {
    fn unroll(self) -> Result<T, RisError>;
}

impl<T> Extensions<T> for Option<T> {
    fn unroll(self) -> Result<T, RisError> {
        match self {
            Some(value) => Ok(value),
            None => Err(RisError::from(OptionError)),
        }
    }
}

#[macro_export]
macro_rules! new {
    ($($arg:tt)*) => {{
        use $crate::error::RisError;
        
        let source = None;
        let message = Some(format!($($arg)*));
        let file = String::from(file!());
        let line = line!();
        let backtrace = $crate::get_backtrace!();
        RisError{source, message, file, line, backtrace}
    }};
}

#[macro_export]
macro_rules! new_result {
    ($($arg:tt)*) => {{
        let result = $crate::new!($($arg)*);
        Err(result)
    }};
}

#[macro_export]
macro_rules! get_backtrace {
    () => {{
        use std::backtrace::Backtrace;
        use std::sync::Arc;
        
        let backtrace = Arc::new(Backtrace::force_capture());
        eprintln!(
            "\u{001B}[93mWARNING\u{001B}[0m: created backtrace. this operation is expensive. excessive use may cost performance.\n    in {}:{}\n",
            file!(),
            line!(),
        );
        
        backtrace
    }}
}

Next Post: I found a Bug in my JobSystem

Programming · Mar 17th, 2024

Next Post: I found a Bug in my JobSystem


Programming · Mar 17th, 2024

Previous Post: My most hated feature in Rust
More Programming related Posts