Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 274
felipou Avatar asked May 25 '26 23:05

felipou


1 Answers

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))?;
like image 69
cdhowie Avatar answered May 30 '26 06:05

cdhowie



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!