Thuta Learning
Rust
IntermediateProgrammingbeginner

Error Handling — Result<T, E> and the ? Operator

What you'll walk away with

  • Explain the core ideas behind Error Handling — Result<T, E> and the ? Operator
  • Run the sample Rust code and verify its output
  • Apply the technique correctly to the Tutorial Platform and production scenarios

Build the mental model

Most languages like Java, Python, or JavaScript handle recoverable errors (a missing file, a network timeout) by throwing exceptions caught with try/catch — the problem with that approach is a function's signature gives no clue which exceptions it might throw, which can surprise the caller. Rust instead extends Lesson 10's `Option<T>` pattern with `Result<T, E>` (`Ok(T)` for success, `Err(E)` for an error), making the possibility of failure explicit right in the function signature — reading `fn read_file(path: &str) -> Result<String, io::Error>` tells you instantly this function can fail. The `?` operator is concise syntax sugar for propagating errors: `let content = read_file(path)?;` unwraps the value if it's `Ok(value)`, or immediately returns `Err(e)` from the current function if it's `Err(e)` — letting error paths flow through a single line instead of requiring a nested match in every function. Rust's philosophy sharply separates two error categories: recoverable errors (`Result`) and unrecoverable errors (`panic!`, covered in more depth in Lesson 20) — a function's type signature itself documents exactly how it can fail.

Connect it to a real scenario

Write the content analyzer's file-reading function as `fn read_lesson(path: &str) -> Result<String, io::Error>` — if the file is missing or a permission issue occurs, the caller is compile-time forced to handle it. In a function scanning a whole directory and processing each file, call `read_lesson(path)?` per file — if one file fails, the whole batch immediately aborts and the error propagates upstream. In a report-generation function, `match read_lesson(path) { Ok(content) => ..., Err(e) => eprintln!("skip {path}: {e}") }` lets you decide to keep processing even if one file fails — choosing between `?` (abort) and explicit `match` (continue) is a real design decision.

Try the working example

rust
use std::fs;
use std::io;

fn read_lesson(path: &str) -> Result<String, io::Error> {
    let content = fs::read_to_string(path)?;
    Ok(content)
}

fn word_count(path: &str) -> Result<usize, io::Error> {
    let content = read_lesson(path)?;
    Ok(content.split_whitespace().count())
}

fn main() {
    match word_count("lessons/ownership.md") {
        Ok(count) => println!("{count} words"),
        Err(e) => eprintln!("failed to read lesson: {e}"),
    }
}
You should see
If the file exists you print its word count; if it doesn't, an error message prints to stderr — either way the program does not crash.

5-minute try-it

Write `fn parse_difficulty(s: &str) -> Result<u32, String>` that compares the input against "basic"/"intermediate"/"advanced" and returns `Ok` with a numeric level (1/2/3) on a match, or a descriptive `Err(String)` otherwise — test it with a caller function that uses the `?` operator.

One important caution

Using `.unwrap()` on a `Result` in production code without handling the error case — even predictable errors like "file not found" or "permission denied" then crash the program with a panic.

Trying to use the `?` operator inside a function whose return type isn't `Result` (like a plain `fn main()`) — `?` only works when the current function's return type is compatible with the error type, otherwise it's a compile error.

The Rust Programming Language — Recoverable Errors with ResultRust

Easy traps

  • Using `.unwrap()` on a `Result` in production code without handling the error case — even predictable errors like "file not found" or "permission denied" then crash the program with a panic.
  • Trying to use the `?` operator inside a function whose return type isn't `Result` (like a plain `fn main()`) — `?` only works when the current function's return type is compatible with the error type, otherwise it's a compile error.
  • Validate sample code in a local or test environment before applying it to a production system.

Exercise

Write `fn parse_difficulty(s: &str) -> Result<u32, String>` that compares the input against "basic"/"intermediate"/"advanced" and returns `Ok` with a numeric level (1/2/3) on a match, or a descriptive `Err(String)` otherwise — test it with a caller function that uses the `?` operator.

You'll know it worked when: If the file exists you print its word count; if it doesn't, an error message prints to stderr — either way the program does not crash.

Error Handling — Result<T, E> and the ? Operator | Thuta Learning