Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first start hitting complex error scenarios in Rust, you'll likely encounter a temptation that I've fallen for myself many times: the urge to just return Result<T, String> or Result<T, Box<dyn std::error::Error>>. It feels efficient. You don't have to write boilerplate, and you can just throw any error message into a string and call it a day. I call this the "Stringly-typed" error pattern, and it's a trap.

The "Just use a String" Trap

Let's look at why this fails in a real-world scenario. Imagine you're writing a library that parses a custom configuration file. You might be tempted to do this:

fn load_config(path: &str) -> Result<Config, String> {
    let content = std::fs::read_to_string(path)
        .map_err(|e| format!("Failed to read file: {}", e))?;
    
    let config = parse_content(&content)
        .map_err(|e| format!("Syntax error at line {}: {}", e.line, e.msg))?;
    
    Ok(config)
}

On the surface, this looks fine. But think about the person using your function. If they want to handle a "File Not Found" error differently than a "Syntax Error" (perhaps by creating a default config file if the first is missing), they can't. They have to resort to string matching—searching for the phrase "Failed to read file"—which is incredibly brittle. One typo in your error message, and their error handling logic breaks. That's a nightmare to maintain.

The Power of Domain-Specific Error Enums

The correct way to handle this is to define your own error type using an enum. This turns your errors into first-class data that the compiler can help you manage. Instead of a vague string, you provide a set of specific, named possibilities.

#[derive(Debug)]
enum ConfigError {
    Io(std::io::Error),
    ParseError { line: usize, column: usize },
    MissingRequiredField(String),
}

Now, when you return a ConfigError, the caller can use a match statement to react precisely to what went wrong. I love this approach because it documents the failure modes of your function right in the type signature. You aren't guessing what might go wrong; the enum tells you exactly what to expect.

Making it Official with the Error Trait

Defining the enum is the first step, but to make your error "play nice" with the rest of the Rust ecosystem, you need to implement std::fmt::Display and std::error::Error. If you don't, you can't use your error with crates like anyhow or tools that expect a standard error object.

Here is how I typically set this up manually. I'll omit the Debug derive for brevity since we already have it:

use std::fmt;

impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ConfigError::Io(err) => write!(f, "IO error: {}", err),
            ConfigError::ParseError { line, column } => {
                write!(f, "Parse error at line {}, col {}", line, column)
            }
            ConfigError::MissingRequiredField(field) => {
                write!(f, "Missing required field: {}", field)
            }
        }
    }
}

impl std::error::Error for ConfigError {
    // We can leave this empty if we don't need to provide a custom 'source'
}

Now your custom error is a fully qualified Rust error. You can wrap it, pass it up the stack, or convert it. In a professional production environment, you'll often see people using a crate called thiserror to automate this boilerplate, but I strongly suggest you write it manually a few times. Understanding that an error is just a type that implements Display and Error is key to mastering Rust's error handling philosophy.




📋 Practical Task

Build a Robust User Account Validator

You are building a registration system. Instead of returning booleans or strings, you need to create a custom error type that identifies exactly why a user's registration failed.

Requirements:

  • Define an enum called ValidationError with the following variants:
    • UsernameTooShort (should include the minimum length required as a usize)
    • InvalidEmail (should include the invalid email string)
    • PasswordTooWeak (should include a message explaining why)
  • Implement std::fmt::Display for ValidationError so that each variant prints a user-friendly error message.
  • Implement std::error::Error for ValidationError.
  • Write a function validate_user(username: &str, email: &str, password: &str) -> Result<(), ValidationError> that:
    • Returns UsernameTooShort if the username is less than 3 characters.
    • Returns InvalidEmail if the email doesn't contain an '@' symbol.
    • Returns PasswordTooWeak if the password is less than 8 characters.
    • Returns Ok(()) if all checks pass.

Test your code: In your main function, call validate_user with a few different invalid inputs and use a match block to print a specific message for each ValidationError variant.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.