felipou
felipou

Reputation: 739

Rust error handling - capturing multiple errors

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.

Upvotes: 9

Views: 5564

Answers (1)

cdhowie
cdhowie

Reputation: 169018

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))?;

Upvotes: 5

Related Questions