What's best practices for converting Error variants?

I need to keep track of errors of my every functions in my program very precisely, thus I define an error type for each function. Most of the error types are enums with no-field variants. It is not the problem.
But, boilerplate code (From and TryFrom impls) about convert an error type to another is too much. Popular error-handling crates allows me to convert an error to the variant of the another, but helps nothing if I'd like to convert an error to the variants of the another by its variant. Why don't these popular crates provide it, is it because the demand is rare or my practice is bad? If the latter, how should I redesign errors in my program?

It is because your demand is rare.

1 Like

Could you please upload some code so we can see what you are doing?

I struggled with error types too. In my case, I found it easier to use fewer error types that are more versatile. Consider how std::io::Error is designed: it basically allows to cover any I/O error (even non-standard (I/O) errors).

I'd like to share a piece of code that I recently used when writing error types for a crate that I have been working on, which allows compiling and executing interpreted language chunks in a sandbox. I am using the following approach there:

/// Kinds of `MachineError`s
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub enum MachineErrorKind {
    /// Out-of-memory error
    Memory,
    /// Syntax error during compilation
    Syntax,
    /// Execution limit error
    ExecLimit,
    /// Generic runtime error
    Runtime,
    /// Unexpected value(s) (e.g. wrong type or wrong count of return values)
    Data,
    /// Error kind unknown
    Unknown,
}

impl Default for MachineErrorKind {
    fn default() -> Self {
        MachineErrorKind::Unknown
    }
}

/// Runtime or compiler errors that may refer to a specific position in code
#[derive(Clone, Default)]
pub struct MachineError {
    /// Kind of runtime error
    pub kind: MachineErrorKind,
    /// Error message
    pub message: Option<String>,
    /// Backtrace (multiple lines)
    pub machine_backtrace: Option<String>,
    /// Chunk name already included in message?
    pub message_incl_chunk_name: bool,
    /// Chunk name
    pub chunk_name: Option<String>,
    /// Position already included in message?
    pub message_incl_pos: bool,
    /// Line number
    pub line: Option<usize>,
    /// Column number (in UTF-8 codepoints)
    pub column: Option<usize>,
    /// Byte position
    pub position: Option<usize>,
}

Methods, which cannot return any other error than a syntax error, for example, will also return a Result<_, MachineError>, even if MachineError::kind will always be set to MachineErrorKind::Syntax (but theoretically could be set to another value – the compiler won't know).

This sacrifices a bit of type safety here, same as writing to a file would (should?) never return a std::io::Error with kind set to ErrorKind::UnexpectedEof. But it seems easier to make every I/O operation return the same type of error, even if that erases some information at compile time (i.e. it erases the info which particular errors could happen).

I'm curious what you think about this approach and how you solved error handling? I felt like when you don't keep things simple, then error handling is a very very time-consuming task in Rust.

edit: removed some irrelevant parts in code excerpt

I don't see what other method than a hand-made match would be simpler/cleaner for your From impl.

If an error is only converted in a few places, its sometimes easier to just use other_result.map_err(MyError::Whatever) and forgo the From/TryFrom impl, map_err is also useful if you want two variants derived from the same error type.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.