9

I've started to learn Rust last week, by reading books and articles, and trying to convert some code from other languages at the same time.

I came across a situation which I'm trying to exemplify through the code below (which is a simplified version of what I was trying to convert from another language):

#[derive(Debug)]
struct InvalidStringSize;
impl std::fmt::Display for InvalidStringSize {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "string is too short")
    }
}
impl std::error::Error for InvalidStringSize {}

pub fn extract_codes_as_ints(
    message: String,
) -> Result<(i32, i32, i32), Box<dyn std::error::Error>> {
    if message.len() < 20 {
        return Err(Box::new(InvalidStringSize {}));
    }
    let code1: i32 = message[0..3].trim().parse()?;
    let code2: i32 = message[9..14].trim().parse()?;
    let code3: i32 = message[17..20].trim().parse()?;
    Ok((code1, code2, code3))
}

So basically I want to extract 3 integers from specific positions of the given string (I could also try to check the other characters for some patterns, but I've left that part out).

I was wondering, is there a way to "catch" or verify all three results of the parse calls at the same time? I don't want to add a match block for each, I'd just like to check if anyone resulted in an error, and return another error in that case. Makes sense?

The only solution I could think of so far would be to create another function with all parses, and match its result. Is there any other way to do this?

Also, any feedback/suggestions on other parts of the code is very welcome, I'm struggling to find out the "right way" to do things in Rust.

3
  • I don't really grasp what you want: you already err when a number isn't parseable, don't you? Commented Apr 9, 2022 at 22:34
  • @ChayimFriedman My understanding is that OP wants to catch the parse error and return a different type error in that case. Commented Apr 9, 2022 at 22:44
  • Yes, exactly as @Lagerbaer said, sorry if I didn’t make myself clear. Another possibility would be to add some error handling code too, like logging or whatnot. Commented Apr 9, 2022 at 23:11

1 Answer 1

6

The idiomatic way to accomplish this is to define your own error type and return it, with a From<T> implementation for each error type T that can occur in your function. The ? operator will use From conversions to match the error type your function is declared to return.

A boxed error is overkill here; just declare an enum listing all of the ways the function can fail. The variant for an integer parse error can even capture the caught error.

use std::error::Error;
use std::fmt::{Display, Formatter, Error as FmtError};
use std::num::ParseIntError;

#[derive(Debug, Clone)]
pub enum ExtractCodeError {
    InvalidStringSize,
    InvalidInteger(ParseIntError),
}

impl From<ParseIntError> for ExtractCodeError {
    fn from(e: ParseIntError) -> Self {
        Self::InvalidInteger(e)
    }
}

impl Error for ExtractCodeError {}

impl Display for ExtractCodeError {
    fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
        match self {
            Self::InvalidStringSize => write!(f, "string is too short"),
            Self::InvalidInteger(e) => write!(f, "invalid integer: {}", e)
        }
    }
}

Aside: if you use the thiserror crate to derive Error, this code becomes way simpler:

use std::num::ParseIntError;

#[derive(Debug, Clone, thiserror::Error)]
pub enum ExtractCodeError {
    #[error("string is too short")]
    InvalidStringSize,
    #[error("invalid integer: {0}")]
    InvalidInteger(#[from] ParseIntError),
}

Now we just need to change your function's return type and have it return ExtractCodeError::InvalidStringSize when the length is too short. Nothing else needs to change as a ParseIntError is automatically converted into an ExtractCodeError:

pub fn extract_codes_as_ints(
    message: String,
) -> Result<(i32, i32, i32), ExtractCodeError> {
    if message.len() < 20 {
        return Err(ExtractCodeError::InvalidStringSize);
    }
    let code1: i32 = message[0..3].trim().parse()?;
    let code2: i32 = message[9..14].trim().parse()?;
    let code3: i32 = message[17..20].trim().parse()?;
    Ok((code1, code2, code3))
}

As an added bonus, callers of this function will be able to inspect errors more easily than with a boxed dyn Error.

In more complex cases, such as where you'd want to tweak the error slightly for each possible occurrence of a ParseIntError, you can use .map_err() on results to transform the error. For example:

something_that_can_fail.map_err(|e| SomeOtherError::Foo(e))?;
Sign up to request clarification or add additional context in comments.

1 Comment

That was very insightful! Thanks!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.