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
74: Error Handling Crates: anyhow and thiserror
By now, you've probably spent a fair amount of time wrestling with Result and the Error trait. If you've built anything larger than a script, you've likely felt the friction: you either spend half your time writing boilerplate From implementations to convert one error type to another, or you lazily map everything to a String and lose all your type safety.
In the real world, we solve this using two crates: thiserror and anyhow. The golden rule I follow is this: use thiserror when you're building a library (where the caller needs to react to specific error variants), and use anyhow when you're building an application (where you mostly just want to report the error and crash or log it).
Let's build a small configuration loader to see this in action. We want to read a config.toml file and ensure it contains a required "version" key.
Defining structured errors with thiserror
Since the logic for loading a config is "domain logic," I want it to be precise. I don't want the caller to just get a generic "something went wrong" message; they need to know if the file was missing or if the content was malformed. This is where thiserror shines—it lets us define a custom enum without writing the Display or Error traits by hand.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("could not read config file: {0}")]
Io(#[from] std::io::Error),
#[error("config file is missing the required 'version' key")]
MissingVersion,
#[error("invalid version format: {0}")]
InvalidVersion(String),
}
fn load_config(path: &str) -> Result {
let content = std::fs::read_to_string(path)?; // The #[from] handles the conversion from io::Error
if !content.contains("version =") {
return Err(ConfigError::MissingVersion);
}
Ok(content)
}
Notice that #[from] attribute on the Io variant. That's the magic. It automatically implements From<std::io::Error> for ConfigError, meaning the ? operator just works.
The friction of strict types in the application layer
Now, let's try to use this in our main function. This is where I usually make my first mistake. I tend to try and keep everything "pure" and use my custom error type everywhere.
fn main() -> Result<(), ConfigError> {
let config = load_config("config.toml")?;
println!("Loaded: {}", config);
Ok(())
}
This works fine... until I decide to add a second step to my program, like creating a log directory using std::fs::create_dir. Suddenly, main is returning a Result<(), ConfigError>, but create_dir returns a std::io::Error. Now I'm stuck. I have to go back to my ConfigError enum and add a new variant just to handle a directory creation error that has absolutely nothing to do with the config file logic. It's tedious and pollutes my domain model.
Smoothing things over with anyhow
This is exactly why anyhow exists. In the application layer (the "glue" code), we don't actually care about the specific type of error—we just want to propagate it up to the user. anyhow::Result is essentially a type-erased wrapper that can hold any error that implements std::error::Error.
Let's refactor main to use anyhow. I'll keep load_config exactly as it is, because that logic should remain portable and precise.
use anyhow::{Context, Result};
fn main() -> Result<()> {
// We can wrap the call with .context() to add a high-level explanation
let config = load_config("config.toml")
.context("Failed to initialize application configuration")?;
println!("Loaded: {}", config);
// Now, this io::Error just "fits" into anyhow::Result without
// needing to modify our ConfigError enum.
std::fs::create_dir("logs")
.context("Could not create logs directory")?;
Ok(())
}
The .context() method is the killer feature here. Instead of just getting "No such file or directory," the user sees: Failed to initialize application configuration: could not read config file: No such file or directory. It creates a causal chain of errors that makes debugging a breeze.
To summarize my workflow: thiserror for the "what" (the specific failure modes of a component), and anyhow for the "where" (the context of where that failure happened in the app).
📋 Practical Task
Build a Robust System Health Checker
Your task is to build a small CLI tool that checks the health of a system by reading a "status" file and verifying a "heartbeat" timestamp. You will need to combine both thiserror and anyhow.
- The Library Part: Create a
HealthErrorenum usingthiserror. It should have variants forIoError(using#[from]),InvalidTimestamp(with aStringfield), andSystemOverloaded. - The Logic: Write a function
check_health(path: &str) -> Result<bool, HealthError>. It should read the file; if the file contains the word "CRITICAL", returnSystemOverloaded; if it doesn't contain a digit, returnInvalidTimestamp. - The App Part: In
main, useanyhow::Result. Callcheck_healthand use.context()to provide a user-friendly message. Also, attempt to write the result of the check to a file calledhealth_log.txtusingstd::fs::write, allowinganyhowto handle the potential IO error without modifying yourHealthErrorenum.
There are no comments for now.