Thuta Learning
Rust
ExercisesProgrammingbeginner

Exercise — Design an Error-Handling Strategy

What you'll walk away with

  • Explain the core ideas behind Exercise — Design an Error-Handling Strategy
  • Run the sample Rust code and verify its output
  • Apply the technique correctly to the Tutorial Platform and production scenarios

Build the mental model

Using Lesson 11's `Result<T, E>` in a real project raises a practical design question: how should the error type `E` itself be designed? When functions have distinct error sources (`io::Error` from a file read, a custom parse error, a validation error), a custom `enum AnalyzerError { Io(io::Error), Parse(String), Validation(String) }` that represents all of them is the right call — a direct application of Lesson 10's enum concept to the error-handling domain. Implementing the `std::error::Error` trait (Lesson 14's trait concept) on this custom enum (`impl std::error::Error for AnalyzerError {}` plus `Display`) makes it interoperable with the standard error-handling ecosystem (logging libraries, `Box<dyn Error>` return types) — a pattern most library and application code expects as standard. Implementing `From<io::Error> for AnalyzerError` lets the `?` operator keep working across function boundaries — if an inner function returns `io::Error` but the outer function's error type is `AnalyzerError`, `?` performs the conversion automatically, no manual `.map_err(...)` needed — a direct extension of how Lesson 11's `?` operator scales into a real, multi-layer application. Designing custom error types is a core library-authoring skill — popular Rust crates like `thiserror` and `anyhow` mostly just reduce this boilerplate, without changing the underlying concept: making an error type an explicit piece of data.

Connect it to a real scenario

For the content analyzer module, create `enum AnalyzerError { Io(io::Error), EmptyFile(String) }` and implement `Display`, formatting error messages to be human-readable. Write `impl From<io::Error> for AnalyzerError { fn from(e: io::Error) -> Self { AnalyzerError::Io(e) } }` so the `?` operator inside the file-reading function (`fs::read_to_string(path)?`) gets automatic conversion. Chain two function layers with `fn analyze(path: &str) -> Result<u32, AnalyzerError> { let content = fs::read_to_string(path)?; if content.is_empty() { return Err(AnalyzerError::EmptyFile(path.to_string())); } Ok(count_words(&content)) }` — the single `?` operator seamlessly handles both error kinds.

Try the working example

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

#[derive(Debug)]
enum AnalyzerError {
    Io(io::Error),
    EmptyFile(String),
}

impl fmt::Display for AnalyzerError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AnalyzerError::Io(e) => write!(f, "io error: {e}"),
            AnalyzerError::EmptyFile(path) => write!(f, "empty file: {path}"),
        }
    }
}

impl std::error::Error for AnalyzerError {}

impl From<io::Error> for AnalyzerError {
    fn from(e: io::Error) -> Self {
        AnalyzerError::Io(e)
    }
}

fn analyze(path: &str) -> Result<u32, AnalyzerError> {
    let content = fs::read_to_string(path)?;
    if content.is_empty() {
        return Err(AnalyzerError::EmptyFile(path.to_string()));
    }
    Ok(content.split_whitespace().count() as u32)
}

fn main() {
    match analyze("missing.md") {
        Ok(count) => println!("{count} words"),
        Err(e) => println!("error: {e}"),
    }
}
You should see
When the file doesn't exist, you print "error: io error: ...", proving the ? operator automatically converted io::Error into AnalyzerError.

5-minute try-it

Add a new `TooManyWords(u32)` variant to `AnalyzerError` — modify `analyze` to return this error when the word count exceeds 5000 — add a matching message for this variant in the `Display` implementation.

One important caution

Blanket-using a single `String` error type (like `Result<T, String>`) — if the caller needs to handle error kinds differently (say, running retry logic only for IO errors), it forces fragile string-parsing; a custom enum preserves that information properly.

Trying to use the `?` operator directly across multiple layers without implementing `From<InnerError> for OuterError` — this triggers an error-type-mismatch compile error; either a manual `.map_err(AnalyzerError::Io)` conversion or a `From` implementation is needed.

Standard Library — std::error::ErrorRust

Easy traps

  • Blanket-using a single `String` error type (like `Result<T, String>`) — if the caller needs to handle error kinds differently (say, running retry logic only for IO errors), it forces fragile string-parsing; a custom enum preserves that information properly.
  • Trying to use the `?` operator directly across multiple layers without implementing `From<InnerError> for OuterError` — this triggers an error-type-mismatch compile error; either a manual `.map_err(AnalyzerError::Io)` conversion or a `From` implementation is needed.
  • Validate sample code in a local or test environment before applying it to a production system.

Exercise

Add a new `TooManyWords(u32)` variant to `AnalyzerError` — modify `analyze` to return this error when the word count exceeds 5000 — add a matching message for this variant in the `Display` implementation.

You'll know it worked when: When the file doesn't exist, you print "error: io error: ...", proving the ? operator automatically converted io::Error into AnalyzerError.

Exercise — Design an Error-Handling Strategy | Thuta Learning