I have implemented a parser in Rust that can return different types of errors. Previously I had implemented it using different Structs without fields that implemented the std::error::Error
trait. The issue is that I encountered two problems:
Box<dyn Error>
, which I find stilted to check what error was returned since I have to fall back to the .downcast_ref::<SomeCustomError>()
method instead of simply being able to use a match and check what error variant was returned.For both reasons is that I want to implement it with Enums. My question is: knowing that all variants have the fields pos, line, and message, should I repeat the three fields for all Enum variants? Is there another better way to represent the problem?
What I had in mind was the following code block, but honestly having to repeat the same code for all variants seems a bit far-fetched (maybe this could be solved with a macro?):
use std::fmt;
enum MyCustomError {
ErrorOne(usize, usize, String),
ErrorTwo(usize, usize, String)
}
impl fmt::Display for MyCustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
MyCustomError::ErrorOne(pos, line, message) => {
write!(
f,
"{} at line {} position {}",
message, line, pos
)
},
MyCustomError::ErrorTwo(pos, line, message) => {
write!(
f,
"{} at line {} position {}",
message, line, pos
)
},
}
}
}
fn throw_error() -> Result<(), MyCustomError> {
Err(MyCustomError::ErrorOne(1, 22, String::from("Error one occurred")))
}
fn main() {
// Now I can simply use match! :D
match throw_error() {
Ok(()) => println!("It works!"),
Err(err) => {
match err {
MyCustomError::ErrorOne(..) => println!("Error one -> {}!", err),
MyCustomError::ErrorTwo(..) => println!("Error two -> {}!", err),
}
}
}
}
Another alternative could be using Snafu:
use snafu::Snafu;
#[derive(Debug, Snafu)]
enum MyEnum {
#[snafu(display("{} at line {} position {}", message, line, pos))]
ErrorOne {
pos: usize,
line: usize,
message: String,
},
#[snafu(display("{} at line {} position {}", message, line, pos))]
ErrorTwo {
pos: usize,
line: usize,
message: String,
},
}
fn throw_error() -> Result<(), MyEnum> {
Err(MyEnum::ErrorOne {
line: 1,
pos: 22,
message: String::from("Error one occurred"),
})
}
fn main() {
// Now I can simply use match! :D
match throw_error() {
Ok(()) => println!("It works!"),
Err(err) => match err {
MyEnum::ErrorOne { .. } => println!("Error one -> {}!", err),
MyEnum::ErrorTwo { .. } => println!("Error two -> {}!", err),
},
}
}
But I fall into the same problem of repeating the code for each variant.
Maybe I'm oversimplifying, but two approaches come to my mind:
Use a struct
as follows:
struct MyErr {
line: usize,
pos: usize,
actual_err: MyCustomError,
}
Then, you can still match
on the fild actual_err
impl fmt::Display for MyCustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write(f, "{} ", match self.actual_err {
ErrorOne => "Error 1 Message",
ErrorTwo => "Error 2 Message",
})?;
write!(
f,
"at line {} position {}",
self.line, self.pos
)
}
}
}
You could actually try to generate your error variants and the associated match
statement via macros. Or look into enum_dispatch
that could save you some boilerplate.