RustArena

Step 7 of 7

Handling bad input with match

.expect is fine when bad input means something is badly wrong. But people type the wrong thing all the time: abc, an empty line, 4.5 where a whole number was expected. A good program reacts to that instead of crashing.

parse returns a Result: either Ok(value), holding the number, or Err(error), describing what went wrong. .expect takes the value out of Ok and crashes on Err. To decide for yourself what happens, use match:

fn main() {
    let inputs = ["42", "-7", "abc", "", "99999999999"];
    for text in inputs {
        match text.parse::<i32>() {
            Ok(n) => println!("{text:?} -> number {n}"),
            Err(e) => println!("{text:?} -> error: {e}"),
        }
    }
}
"42" -> number 42
"-7" -> number -7
"abc" -> error: invalid digit found in string
"" -> error: cannot parse integer from empty string
"99999999999" -> error: number too large to fit in target type

The compiler checks that you handle both cases. Leave out the Err arm and the program doesn't compile:

fn main() {
    let text = "42";
    match text.parse::<i32>() {
        Ok(n) => println!("{n}"),
    }
}
error[E0004]: non-exhaustive patterns: `Err(_)` not covered

match produces a value

Like if, match is an expression. Each arm can give a value, and an arm can also jump away with continue, break or return:

fn main() {
    let lines = ["5", "ten", "7", ""];
    let mut total = 0;
    for text in lines {
        let qty: i32 = match text.trim().parse() {
            Ok(n) => n,
            Err(_) => {
                println!("skipping {text:?}");
                continue;
            }
        };
        total += qty;
    }
    println!("total: {total}");
}
skipping "ten"
skipping ""
total: 12

Ok(n) => n makes the whole match evaluate to the number, which lands in qty. The Err arm never produces a value: it prints a note and moves on to the next line with continue. This "use it or skip it" shape is very common.

Asking again

For interactive programs, combine match with loop and break value (from the control module) to ask until the answer makes sense:

use std::io;

fn main() {
    let age: u32 = loop {
        println!("How old are you?");
        let mut line = String::new();
        let bytes = io::stdin().read_line(&mut line).expect("failed to read a line");
        if bytes == 0 {
            println!("No answer, giving up.");
            return;
        }
        match line.trim().parse() {
            Ok(n) => break n,
            Err(_) => println!("'{}' isn't a whole number, try again.", line.trim()),
        }
    };
    println!("Got it: {age}");
}
twelve
12.5
12
How old are you?
'twelve' isn't a whole number, try again.
How old are you?
'12.5' isn't a whole number, try again.
How old are you?
Got it: 12

Note the bytes == 0 check: without it, a program that runs out of input would read empty lines forever, failing to parse each one. Always ask "what happens at the end of the input?".

At work: larger programs rarely match on every parse inside main. They pass errors up to one place that reports them, using Result return types and the ? operator (modules 19 and 39). match on Ok/Err is the building block underneath all of that.

Common mistakes

Your task

Read lines until the end of the input. Each line should be a whole number (an order quantity, negative for a return), but some lines are garbage. Add up the valid numbers, count every other line as skipped (blank lines too), and print:

total: 15
skipped: 2

Challenge 2

Read one line: an age (a whole number, never negative). If it's valid, print the age next year, like next year: 31. Otherwise print what was typed (trimmed), like invalid age: abc.

Challenge 3

A guessing game with a fixed secret number, 7. Read guesses one per line until the end of the input. For each line:

If the input ends without a correct guess, print out of guesses.

Previous: Reading all the input at once