rust - Return a custom error from enum FromStr parsing - Stack Overflow

admin2025-04-20  0

I am implementing FromStr for my custom enum.

I need to prompt the user for the option, so I need to convert it to string (done via fmt::Display), and then when the user selects the option from the list, convert it back to the enum value. Or so I think it should work.

pub enum Options {
    One,
    Two,
    Three,
    Four,
}

impl FromStr for Options {
    type Err = ParseError;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let opt = match s {
            "One" => Self::One,
            "Two" => Self::Two,
            "Three" => Self::Three,
            "Four" => Self::Four,
            _ => return Err(ParseError::new("This option is not available")),
        };
        Ok(opt)
    }
}

I think I have to address an error situation where the parsing does not match any of the option. Although in my current code it's not possible, as the user can only select a valid option, it feels safer to do that. Anyway - good learning, so I want to address the error case.

Thing is, if I use my custom error, then I need to define a lifetime:

pub struct ParseError<'a> {
    cause: &'a str,
}

impl<'a> ParseError<'a> {
    pub fn new(cause: &'a str) -> Self {
        Self { cause }
    }
}

I could use a generic error, but I don't like that, and again, good learning. So. If I go with the lifetime, then the compiler complains at the type alias in FromStr impl due to the missing lifetime parameter.

I have tried

impl FromStr for FilterOptions {
    type Err = ParseError<'a>;

Here the compiler complains with

-  rustc: use of undeclared lifetime name `'a`
   undeclared lifetime [E0261]
- rustc: consider introducing lifetime `'a` here: `<'a>` [E0261]

I understand that I am not understanding lifetimes correctly. I guess the compiler complains that at some point, the lifetime must be specified and made concrete, but I don't get where this needs to be.

I would like to avoid having to drag a lifetime specifier into my enum "just" because of the error.

I found this question How do I return an error within match statement while implementing from_str in rust?, where the solution was to just use an empty struct (no member), which then gets away with the issue as there is no string lifetime involved.

But I am thinking to re-use this ParseError for other enums I have in the code, which I have to parse from string too, so I could just re-use the error and just specify the cause instead of implementing custom errors for everything.

Maybe it's not possible to do this this way because the constraint comes from the trait, which doesn't allow a bound constraints in its declaration?

What's the clean way to do this?

Playground

I am implementing FromStr for my custom enum.

I need to prompt the user for the option, so I need to convert it to string (done via fmt::Display), and then when the user selects the option from the list, convert it back to the enum value. Or so I think it should work.

pub enum Options {
    One,
    Two,
    Three,
    Four,
}

impl FromStr for Options {
    type Err = ParseError;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let opt = match s {
            "One" => Self::One,
            "Two" => Self::Two,
            "Three" => Self::Three,
            "Four" => Self::Four,
            _ => return Err(ParseError::new("This option is not available")),
        };
        Ok(opt)
    }
}

I think I have to address an error situation where the parsing does not match any of the option. Although in my current code it's not possible, as the user can only select a valid option, it feels safer to do that. Anyway - good learning, so I want to address the error case.

Thing is, if I use my custom error, then I need to define a lifetime:

pub struct ParseError<'a> {
    cause: &'a str,
}

impl<'a> ParseError<'a> {
    pub fn new(cause: &'a str) -> Self {
        Self { cause }
    }
}

I could use a generic error, but I don't like that, and again, good learning. So. If I go with the lifetime, then the compiler complains at the type alias in FromStr impl due to the missing lifetime parameter.

I have tried

impl FromStr for FilterOptions {
    type Err = ParseError<'a>;

Here the compiler complains with

-  rustc: use of undeclared lifetime name `'a`
   undeclared lifetime [E0261]
- rustc: consider introducing lifetime `'a` here: `<'a>` [E0261]

I understand that I am not understanding lifetimes correctly. I guess the compiler complains that at some point, the lifetime must be specified and made concrete, but I don't get where this needs to be.

I would like to avoid having to drag a lifetime specifier into my enum "just" because of the error.

I found this question How do I return an error within match statement while implementing from_str in rust?, where the solution was to just use an empty struct (no member), which then gets away with the issue as there is no string lifetime involved.

But I am thinking to re-use this ParseError for other enums I have in the code, which I have to parse from string too, so I could just re-use the error and just specify the cause instead of implementing custom errors for everything.

Maybe it's not possible to do this this way because the constraint comes from the trait, which doesn't allow a bound constraints in its declaration?

What's the clean way to do this?

Playground

Share Improve this question edited Mar 2 at 23:08 John Kugelman 363k69 gold badges553 silver badges597 bronze badges asked Mar 2 at 22:53 unsafe_where_trueunsafe_where_true 6,36217 gold badges67 silver badges123 bronze badges 3
  • 2 FromStr is just not designed to allow this. You can write your own method, but given that this is an error condition anyway so cold path, it's probably better to just use Box<str>. – Chayim Friedman Commented Mar 2 at 23:00
  • I see, ok, thanks. – unsafe_where_true Commented Mar 2 at 23:45
  • Or for the code you've shown us you could also use ParseError<'static> – Jmb Commented Mar 3 at 7:52
Add a comment  | 

1 Answer 1

Reset to default 1

If you haven't already, I'd recomend reading the Rustonomicon page on lifetimes. In essence, named lifetimes let you put a reference inside a struct or function to data that struct or function does not own.

Recall that generic types in Rust work like this:

struct Foo<T> {
    my_data: T,
}

// impls for the type can be only for when the type parameter is a specific type:
impl Foo<i32> {
   fn add_one(&self) -> i32 {
       return self.my_data + 1;
   }
}

// or they can be for any type:
// (I named the type parameter F in this case simply to show that it need not necessarily match the type parameter name in the struct definition.)
// the <F> after the impl means that for the remainder of the impl body, `F` is a type parameter that the compiler can substitute any type into.
impl<F> Foo<F> {
    fn get_ref(&self) -> &F {
        return &self.my_data;
    }
}

fn main() {
   let foo_one: Foo<i32> = Foo {my_data: 5};
   let foo_two: Foo<&str> = Foo {my_data: "hello world"};
   
    let ref_one: &i32 = foo_one.get_ref();
    let ref_two: &&str = foo_two.get_ref();
    let five_plus_one = foo_one.add_one();
    // let string_plus_one = foo_two.add_one(); // this does not compile -- add_one() is defined only for Foo<i32>, not Foo<&str>
}

Named lifetimes work exactly the same way:

struct DataThatIsExpensiveToCopy  {
    // pretend there's a lot of data here that I don't want to copy every time I pass it around
    foo: i32,
    bar: String,
}

struct WrapperStruct<'a> {
    referenced_data: &'a DataThatIsExpensiveToCopy,
}

// it is common practice to use the same lifetime names as in the struct definition, for readability's sake -- and you should in actual code -- but again it is not required.
impl<'whatever> WrapperStruct<'whatever> {
    // obviously, this is a toy example, but it's not hard to imagine a function that does useful work without having to copy the entire type.
    pub fn get_length_of_bar(&self) -> usize {
        self.referenced_data.bar.len()
    }
}


fn main() {
    let data = DataThatIsExpensiveToCopy {
        foo: 5,
        bar = "hello world".to_string(),
     };
     let wrapper = WrapperStruct {referenced_data: &data};
     // wrapper now has a reference to the data.
     // named lifetimes don't *actually* take on the names of the variables they reference, but you could say, for the sake of simplicity, that `wrapper` now has type WrapperStruct<'data>.
     println!("length of bar: {}", wrapper.get_length_of_bar());
     std::mem::drop(data);
     // we can no longer use `wrapper` after this, because it references the data in `data`, and the lifetime `'data` has expired.
     // println!("length of bar is now: {}", wrapper.get_length_of_bar()); // uncommenting this will result in a compile error!
}

Your decision to use a lifetime parameter in an error type that simply holds a string is somewhat unorthodox, but not unheard of. It saves 4 or 8 bytes (depending on if the machine is 32 or 64 bit) when storing the type, and avoids a heap allocation when creating it, although these are not typically important concerns outside of embedded systems. It also has its tradeoffs. Since the data is borrowed rather than owned, and since functions are forbidden from returning references to data owned by the function, you cannot do anything that creates a string dynamically, for example, this:

pub enum Options {
    One,
    Two,
    Three,
    Four,
}

pub struct ParseError<'a> {
    cause: &'a str,
}

impl<'a> ParseError<'a> {
    pub fn new(cause: &'a str) -> Self {
        Self { cause }
    }
}

impl FromStr for Options {
    type Err = ParseError;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let opt = match s {
            "One" => Self::One,
            "Two" => Self::Two,
            "Three" => Self::Three,
            "Four" => Self::Four,
            _ => return Err(ParseError::new(&format!("The option {} is not available", s))), // this does not compile!  format!() creates a new String, which is owned by the function that creates it, and functions cannot return references to owned data.
        };
        Ok(opt)
    }
}

I strongly suspect that what you actually wanted was this:

pub enum Options {
    One,
    Two,
    Three,
    Four,
}

pub struct ParseError {
    cause: String,
}

impl ParseError {
    pub fn new(cause: String) -> Self {
        Self { cause }
    }
}

impl FromStr for Options {
    type Err = ParseError;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        let opt = match s {
            "One" => Self::One,
            "Two" => Self::Two,
            "Three" => Self::Three,
            "Four" => Self::Four,
            _ => return Err(ParseError::new(format!("The option {} is not available", s))),
            // OR
            _ => return Err(ParseError::new("That option is not available".to_string()),
        };
        Ok(opt)
    }
}

The difference is that String creates a heap allocation to hold the data and holds ownership of the data, whereas an &str is, as the & implies, a reference to some data owned by some other object. If you're unfamiliar with this distinction, I'd recommend reading the Rust documentation on ownership to brush up. This other StackOverflow answer has some more info on the difference between String and str.

If you don't care about being able to dynamically generate error messages, however, and saving those 8 bytes and that heap allocation is important to you, you can make your current code work simply by specifying the static lifetime, which is a lifetime name defined by the compiler to mean the lifetime of the entire program:

impl FromStr for Options {
    type Err = ParseError<'static>;
    ...
}

Alternatively, eliminate the lifetime parameter from ParseError altogether:

struct ParseError {
   cause: &'static str;
}

impl ParseError {
   fn new(cause: &'static str) -> Self {
      Self { cause }
   }
}

I should also mention, as long as you're learning, that creating a custom error type in Rust is an extremely common thing to want to do, and there are crates for helping you do it. Two of the most prominent ones are thiserror, which defines error types as enums, one entry for each possible failure mode (useful if you want your errors to be handleable by external code that matches on the error value), and anyhow (or its newer fork eyre), which define all errors as strings (useful if you're too lazy to update the enum and write new formatting code every time you add a new failure mode to a function -- which is entirely reasonable, especially in Rust code!) This response is already obscenely long or I would go into more detail on them. One thing I will mention, though, is that thiserror is a common use case for named lifetimes. If the function returning an error is allowed to reference the lifetime of its input (which, it should be noted, FromStr is not!), you can do something like this:

#[derive(thiserror::Error)]
enum Error<'a> {
    #[error("Parsed it okay, but failed to do a thing with the parse")]
    ErrorDoingAThing,
    #[error("Failed to parse the string {0}")]
    ParseError(&'a str),
}

fn do_something_with_a_string<'a>(&'a str) -> Result<SomeUsefulType, Error<'a>> {
   ...
}

Please don't hesitate to ask for clarification if anything I just said didn't make sense. I don't always make sense on the first try, and I want to do better.

转载请注明原文地址:http://conceptsofalgorithm.com/Algorithm/1745115138a285816.html

最新回复(0)