Search code examples
rustrust-macros

Best strategy for custom errors with common fields in Rust


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.


Solution

  • 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.