0

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:

  1. The parse function returns a 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.
  2. Now I want all errors to report, with a custom message, line number, and position of the text where the error occurs. This item is easily solved with Structs, but I still fall into the previous problem.

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.

Genarito
  • 3,027
  • 5
  • 27
  • 53
  • 1
    Is there a reason not to go with the obvious `enum MyErrorKind { ErrorOne, ErrorTwo(extra_data: u8) }; struct MyError { pos: usize, line: usize, message: String, kind: MyErrorKind }`? – user2722968 Nov 21 '21 at 20:23
  • Thanks for the suggestion. I've thought about it before, but the fact that it's up to the user to check for the "kind" attribute seemed to me to add a boilerplate to the usage instead of simply comparing with a match as [officially proposed](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#matching-on-different-errors). Maybe It's the simpler solution and I could implement PartialEq to compare directly with an error kind Enum – Genarito Nov 22 '21 at 21:05
  • 2
    `PartialEq` doesn't give you structural equality, so you can't `match` on it with the compiler checking all possible variants. But you can simply make the fields `pub`, which allows the user to `match` on it. See [here](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=472f8acc1c1f5271cb01ad9b57441b4f) – user2722968 Nov 22 '21 at 23:43
  • I didn't know _Pattern binding_. Thank you very much! – Genarito Nov 23 '21 at 14:07

1 Answers1

0

Maybe I'm oversimplifying, but two approaches come to my mind:

  1. 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
          )
        }
     } 
    }
    
  2. 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.

phimuemue
  • 34,669
  • 9
  • 84
  • 115
  • Thank you for the answer! It seems to be the simplest way, I wanted to avoid having the error type as an attribute so I could match directly with the Enum MyCustomError as it does in the [official documentation](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#matching-on-different-errors). As I said in my reply to a comment above, maybe I will implement the PartialEq trait to be able to match with the Enum directly. – Genarito Nov 22 '21 at 21:10