Convert a string to number and handle possible errors in Rust

parse i8 u8 f32 casting turbofish expect unwrap unwrap_or unwrap_or_default match Ok Err

No matter where input comes from (the keyboard, a file, the network) - type-wise it is seen as some kind of a string or series of characters. Even if the program asked for a number and the user provided a number.

In order to work with the value and use numerical operations on that value in most programming language we need to somehow convert the string into a number type. In Rust there are a number of numeric types:

  • u8, u16, u32, u64, u128 are unsigned integers using the given number of bits. For example u8 uses 8 bits (1 byte) and it can store a value between 0 and 2^8-1 (that is between 1 and 255).

  • i8, i16, i32, i64, i128 are signed integers using the given number of bits. For example i8 uses 8 bits (1 byte) and it can store a value between -(2^7) and 2^7-1 (that is between -128 and +127).

  • f32, f64 floating point numbers according to the IEEE 754-2008 specification.

  • There are also isize and usize.

In some languages converting a string to a number is called casting, in Rust it is accomplished by calling the parse() method of a string.

Code example

examples/convert-string-to-number/src/main.rs


fn main() {
    let argv: Vec<String> = std::env::args().collect();
    if argv.len() != 2 {
        eprintln!("Usage: {} value", argv[0]);
        std::process::exit(1);
    }

    let value = &argv[1];

    let num: i16 = value.parse().expect("Could not convert");
    //let num = value.parse::<i16>().expect("Could not convert");

    //let num: i16 = value.parse().unwrap();

    //let num: i16 = value.parse().unwrap_or(42);

    //let num: i16 = value.parse().unwrap_or_default();


    // let num: i16 = match value.parse() {
    //     Ok(val) => val,
    //     Err(err) => {
    //         eprintln!("Could not convert: '{err}'");
    //         return;
    //     }
    // };

    println!("Converted to {}", num);
}

Feel free to use this example, enable/disable parts of the code to see the various solutions for error handling.

In the first part of the example we expect and accept a value on the command line. See the explanation in that other article.

Parse

The parse method needs to know what numerical type it is supposed to create. There are two ways to accomplish this. In one way we define the type of the variable that we are assigning to, in this case we used i16.

let num: i16 = value.parse().expect("Could not convert");

In the second case we use the so-called turbofish syntax, where we put the type after the function in ::<>.

let num = value.parse::<i16>().expect("Could not convert");

Good results

If we run cargo run 42 we will see Converted to 42. That's the happy path. What about error handling?

Error handling

An i16 can hold an integer number between -(2^15) and (2^15)-1 that is between -32768 and +32767. For any string that holds some other value the operation will fail. So if we try to convert 3.14, 10,000,000, or "abc" and even 32768 will fail. The question then, how can we deal with this in our code.

In the example below I'll stick to the first version, but they could have been done with the turbofish syntax as well.

expect

The first way we saw in the examples above was to add a call to expect to be executed on the result of parse(). This is an easy, but not a recommended solution. It is easy as it only requires us to add a few letters, but it is not recommended as this will cause the program to panic in case the parse() fails. That is, if we supply any value that cannot be converted to i16 our program will die.

For example:

cargo run 3.14


thread 'main' panicked at 'Could not convert: ParseIntError { kind: InvalidDigit }', src/main.rs:11:34
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

In the error message you can see the text we included in the call to expect.

Unlike exceptions in other programming languages, you cannot "catch" a panic in Rust. You should probably want to avoid it.

unwrap

Instead of expect we could call unwrap, but that seems to be worse. It includes a generic error message instead of one we decided upon.

let num: i16 = value.parse().unwrap();
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }', src/main.rs:14:34
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

unwrap_or

let num: i16 = value.parse().unwrap_or(42);

The call to unwrap_or allows us to provide a default value. Is it a good idea to provide a default value? Some cases it might be. In other cases probably not. Using 42 as the default value for a numerical conversion is clearly not a very good idea as you can see from the results:

$ cargo run 3
Converted to 3

$ cargo run 3.1
Converted to 42

unwrap_or_default

let num: i16 = value.parse().unwrap_or_default();

We could use unwrap_or_default that, in case of parsing error, would use the default value of numbers which is 0.

match / Ok / Err

Finally let's see the fully coded solution using the match keyword:

let num: i16 = match value.parse() {
    Ok(val) => val,
    Err(err) => {
        eprintln!("Could not convert: '{err}'");
        return;
    }
};

In this solution we are using match to handle the two possible return values of the parse function that can be returned as two different cases of a Result type.

The happy case, when the parsing is successful is when get back the parse value wrapped in Ok. If Ok(val) matches then we are only interested in the value that is in val. (Here Ok was the keyword and val was just an temporary variable name.)

In case the parsing fails we get back an error object wrapped in an Err. In this case we print some error message and then return from this call, which in our case effectively means the program stops. How exactly you deal with the case of failure is of course up to you. This is just one way to handle it.

Author

Gabor Szabo (szabgab)

Gabor Szabo, the author of the Rust Maven web site maintains several Open source projects in Rust and while he still feels he has tons of new things to learn about Rust he already offers training courses in Rust and still teaches Python, Perl, git, GitHub, GitLab, CI, and testing.

Gabor Szabo