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
:
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
}}
}
|
◀ | Previous Post: My most hated feature in Rust |
▶ | More Programming related Posts |