Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
29: Custom Error Types
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
ValidationErrorwith the following variants:UsernameTooShort(should include the minimum length required as ausize)InvalidEmail(should include the invalid email string)PasswordTooWeak(should include a message explaining why)
- Implement
std::fmt::DisplayforValidationErrorso that each variant prints a user-friendly error message. - Implement
std::error::ErrorforValidationError. - Write a function
validate_user(username: &str, email: &str, password: &str) -> Result<(), ValidationError>that:- Returns
UsernameTooShortif the username is less than 3 characters. - Returns
InvalidEmailif the email doesn't contain an '@' symbol. - Returns
PasswordTooWeakif the password is less than 8 characters. - Returns
Ok(())if all checks pass.
- Returns
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.
There are no comments for now.