Handle Recoverable Errors with Result

RustBeginner
Practice Now

Introduction

Some failures are programming bugs, but others are expected at runtime: a person may enter malformed text or a value outside an allowed range. Rust represents these recoverable outcomes with Result<T, E>, which is either Ok(T) or Err(E).

You will parse valid and invalid text without panicking, return useful validation errors, and use the ? operator to pass failures to a caller. The prepared Cargo project keeps every edit focused on error handling.

Match a Successful Result

In this step, you will parse text into an integer and match the successful result.

The project is /home/labex/project/result-workshop, and src/main.rs contains four focused placeholders. Enter the project and open the source:

cd /home/labex/project/result-workshop
nano src/main.rs

Replace the Step 1 comment with:

    let valid = "42".parse::<i32>();
    match valid {
        Ok(value) => println!("Parsed value: {value}"),
        Err(error) => println!("Parse error: {error}"),
    }

parse is a reusable method that can produce different output types. The ::<i32> part supplies the output type for this call, asking Rust to interpret the string as a signed 32-bit integer. This angle-bracket type argument is different from a path such as Message::Text, even though both use ::. Parsing can fail, so the returned type is a Result. Ok(value) carries the parsed integer; Err(error) carries information about why parsing failed. A match must handle both variants.

Save with Ctrl+O, press Enter, and exit with Ctrl+X. Check and run:

cargo check
cargo run --quiet
Parsed value: 42

Only the Ok arm ran because "42" is valid integer text.

Recover from Invalid Input

In this step, you will handle malformed text as data instead of allowing it to crash the program.

A panic indicates that a program reached a state its design did not expect and usually stops execution. Malformed external input is expected, so it should remain an Err that your program can inspect and report. Open the source:

nano src/main.rs

Replace the Step 2 comment with:

    let invalid = "many".parse::<i32>();
    match invalid {
        Ok(value) => println!("Unexpected value: {value}"),
        Err(error) => println!("Handled invalid input: {error}"),
    }

This match still covers both possible results, but the prepared text selects the Err arm. Save and exit, then run:

cargo run --quiet

The second line should be:

Handled invalid input: invalid digit found in string

The program continued normally and converted the parser's error into a useful message. Avoid unwrap() for external data: unwrap() would panic on this same Err instead of letting you recover.

Parse and Validate with the Question Mark Operator

In this step, you will create a function that can return either a percentage or a useful error message.

The type Result<u32, String> means success contains an unsigned integer and failure contains owned error text. The ? operator takes the value from Ok; if it sees Err, it immediately returns that error from the current function.

Open the source:

nano src/main.rs

Add this function above main:

fn parse_percentage(text: &str) -> Result<u32, String> {
    let value = text
        .parse::<u32>()
        .map_err(|_| format!("not a whole number: {text}"))?;

    if value <= 100 {
        Ok(value)
    } else {
        Err(format!("outside 0..=100: {value}"))
    }
}

Read the function as a short sequence. First, parse::<u32>() tries to turn the text into a whole number. Then map_err(...) changes only a parse failure into a clearer String containing the original input.

Next, ? either unwraps the successful number for value or returns that new error immediately from parse_percentage. If parsing succeeds, the if validates the number and returns either Ok(value) or an out-of-range Err.

The closure parameter _ means the original parser error value is deliberately ignored because the new message replaces it. Here _ is an ignored parameter; earlier, _ => was a wildcard match arm and let _ = discarded a complete result.

Replace the Step 3 comment inside main with:

    println!("85 => {:?}", parse_percentage("85"));
    println!("150 => {:?}", parse_percentage("150"));
    println!("many => {:?}", parse_percentage("many"));

The :? formatter reveals the Ok or Err variant for this learning experiment. Save and exit, then check and run:

cargo check
cargo run --quiet

The new lines should be:

85 => Ok(85)
150 => Err("outside 0..=100: 150")
many => Err("not a whole number: many")

The function now distinguishes a valid value, a parsed but out-of-range value, and malformed text.

Propagate an Error Through Another Function

In this step, you will use ? at a second function boundary and handle the final result in main.

A helper does not always know how an application should display or recover from an error. It can propagate the Err upward, leaving the caller to decide. Open the source:

nano src/main.rs

Add this function between parse_percentage and main:

fn acceptance_message(text: &str) -> Result<String, String> {
    let value = parse_percentage(text)?;
    Ok(format!("accepted {value}%"))
}

If parsing or validation fails, ? returns the existing String error immediately. On success, the function wraps its message in Ok.

Replace the Step 4 comment inside main with:

    for text in ["73", "bad"] {
        match acceptance_message(text) {
            Ok(message) => println!("{text} => {message}"),
            Err(message) => println!("{text} => error: {message}"),
        }
    }

The loop supplies one successful and one failing input. main is the boundary that turns each result into user-facing output. Save and exit, then check and run:

cargo check
cargo run --quiet

The final lines should be:

73 => accepted 73%
bad => error: not a whole number: bad

The same error context created by parse_percentage survived propagation through acceptance_message.

Summary

You matched successful and failed Result values, treated malformed input as a recoverable condition, created contextual String errors with map_err, and propagated failures through function boundaries with ? instead of panicking with unwrap.