---
title: "12.4 Handling invalid input properly"
description: "Turning the guessing game's expect crash into a polite retry loop, and a taxonomy of the ways user input goes wrong: malformed, out of range, and absent."
url: "https://learnrust.net/chapter-12/handling-invalid-input/"
last_updated: "2026-06-13"
---

# 12.4 Handling invalid input properly

Lesson [7.10](https://learnrust.net/chapter-7/project-number-guessing-game/) left an IOU. The guessing game crashed when you typed `banana` instead of a number, because `parse().expect(...)` panics on bad input, and we promised chapter 12 would turn that crash into a polite retry. The promise comes due. This lesson uses `Result` and the pattern-matching tools to handle user input the way real programs do: by anticipating every way it can go wrong and responding, not crashing.

## Why input is the classic recoverable error

User input is the textbook recoverable failure. People mistype, paste the wrong thing, hit enter by accident. None of that is a bug in your program, so none of it should panic. A program that crashes because someone fat-fingered a digit is a badly behaved program. The right response is almost always "tell them what went wrong and let them try again," and `Result` is what makes that response systematic instead of a pile of ad-hoc checks.

The ways a single line of typed input can be wrong form a small taxonomy worth naming. It can be **malformed**: not the kind of thing you asked for at all (`banana` when you wanted a number). It can be **out of range**: the right kind but an unacceptable value (`500` when you wanted 1 to 100). And the parse can encounter **extraction failure**: reading the line itself fails, or there's nothing there. Each is a distinct case, and good input handling addresses all of them.

## From crash to retry

Here's the guessing game's input step as lesson [7.10](https://learnrust.net/chapter-7/project-number-guessing-game/) left it, the version that crashes:

```rust
let guess: u32 = guess.trim().parse().expect("Please type a number!");
```

`parse` returns a `Result` (lesson [12.2](https://learnrust.net/chapter-12/result/)), and `expect` turns any `Err` into a panic (lesson [12.1](https://learnrust.net/chapter-12/panic-and-unrecoverable-errors/)). To recover instead of crash, replace the `expect` with a `match` that handles the `Err` by skipping the bad input and looping again. The Rust Book's exact fix:

```rust
use std::io;

fn main() {
    println!("Guess the number (1-100):");

    loop {
        let mut guess = String::new();
        io::stdin().read_line(&mut guess).expect("failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => {
                println!("That wasn't a number. Try again.");
                continue;
            }
        };

        println!("You guessed: {guess}");
        break;
    }
}
```

```
Guess the number (1-100):
banana
That wasn't a number. Try again.
42
You guessed: 42
```

The whole transformation is in that `match`. On `Ok(num)`, we keep the number and carry on. On `Err(_)`, instead of panicking, we print a friendly message and `continue` (lesson [7.6](https://learnrust.net/chapter-7/break-continue-and-loop-labels/)) back to the top of the loop to ask again. Typing `banana` no longer ends the program; it costs the user one polite reminder. This is the difference between handling an error and exploding on it, and it's two lines of difference. (The `expect` on `read_line` stays, because a failure to read stdin at all is a genuinely unusual situation, not ordinary bad input; lesson [12.5](https://learnrust.net/chapter-12/when-to-panic/) sharpens that judgment.)

## Handling the out-of-range case too

That `match` handles *malformed* input. The *out of range* case (a number, but not 1 to 100) is a separate check, and it's where the `Ok` arm earns a guard or a follow-up test:

```rust
let guess: u32 = match guess.trim().parse() {
    Ok(num) if (1..=100).contains(&num) => num,
    Ok(_) => {
        println!("Please enter a number from 1 to 100.");
        continue;
    }
    Err(_) => {
        println!("That wasn't a number. Try again.");
        continue;
    }
};
```

Now three outcomes are handled distinctly: a valid in-range number passes through, an out-of-range number gets its own message, and non-numbers get theirs. The guard `if (1..=100).contains(&num)` (a range method that tests membership, lesson [7.5](https://learnrust.net/chapter-7/for-loops-and-ranges/)'s ranges with a method on top) splits the two `Ok` cases. This is the taxonomy turned into code: each way the input can be wrong gets its own arm and its own message, which is exactly what a user wants, a specific reason rather than a generic rejection.

> **Best practice**
>
> Validate input as close to where you read it as possible, and give each failure its own message. "That wasn't a number" and "that's out of range" are different problems and deserve different responses; collapsing them into one "invalid input" wastes the information you have. The `match`-on-`parse` with guards is the standard shape for this, and `continue` in a loop is the standard "let them retry."

## Factoring it into a function

As the validation grows, it wants to be its own function, and now `Result` and `?` (lesson [12.3](https://learnrust.net/chapter-12/propagating-errors-question-mark-operator/)) pay off. A function that reads and validates one guess, returning a `Result`, keeps `main`'s loop clean:

```rust
use std::io;

fn read_guess() -> Result<u32, String> {
    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .map_err(|_| String::from("could not read input"))?;

    let num: u32 = input
        .trim()
        .parse()
        .map_err(|_| String::from("not a number"))?;

    if (1..=100).contains(&num) {
        Ok(num)
    } else {
        Err(String::from("out of range (1-100)"))
    }
}

fn main() {
    loop {
        match read_guess() {
            Ok(guess) => {
                println!("You guessed: {guess}");
                break;
            }
            Err(reason) => println!("{reason}, try again."),
        }
    }
}
```

```
You guessed: 42
```

`read_guess` returns `Result<u32, String>`: a valid guess or a reason it failed. The two `?`s propagate read and parse failures, converting each error into a readable `String` with `map_err` (a method that transforms the error inside an `Err`, leaving an `Ok` untouched; the `|_| ...` is a closure, chapter 19's subject, here just "ignore the original error and use this message"). `main` calls it in a loop and decides what to do with each outcome. The validation logic lives in one place, `main` reads cleanly, and every failure mode is still handled. This is the shape real input handling takes once it outgrows a single `match`.

## Quiz time

**Question #1**

What single change turns `parse().expect("...")` from a crash-on-bad-input into a retry, and what does that change rely on?

<details class="solution">
<summary>Show solution</summary>

Replace the `expect` with a `match` on the `Result`: keep the value on `Ok`, and on `Err` print a message and `continue` the loop instead of panicking. It relies on `parse` returning a `Result` (lesson [12.2](https://learnrust.net/chapter-12/result/)), so the failure is a value you can branch on rather than an explosion.

</details>

**Question #2**

Name the three categories in the input-failure taxonomy, with an example of each for "enter a number from 1 to 100."

<details class="solution">
<summary>Show solution</summary>

Malformed (not the right kind of thing): `banana`. Out of range (right kind, bad value): `500`. Extraction failure (couldn't read or nothing there): the read itself fails, or empty input. Good handling gives each its own response.

</details>

**Question #3**

Why does the example keep `.expect("failed to read line")` on `read_line` while replacing `expect` on `parse`?

<details class="solution">
<summary>Show solution</summary>

A `parse` failure is ordinary, expected bad input (the user typed letters), so it deserves recovery, not a panic. A `read_line` failure means reading from standard input itself broke, which is rare and not something a retry loop can sensibly fix in a simple program, so panicking (or propagating) is defensible. Lesson [12.5](https://learnrust.net/chapter-12/when-to-panic/) makes this "expected versus exceptional" judgment the explicit rule.

</details>

That last quiz answer is the whole next lesson: how do you decide whether a given failure deserves a `Result` or a `panic!`? It comes down to a few clear guidelines, and getting it right is what separates code that crashes rudely from code that fails gracefully.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
