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
161: Error Context and Chaining
We've spent a lot of time talking about Result and how to propagate errors using the ? operator. But here is the reality of production software: a raw std::io::Error that says "No such file or directory" is almost useless when your application is trying to load twelve different configuration files across three different directories. You need to know which file failed and why it mattered to the program.
In this lesson, I want to show you how to add "context" to your errors. While you can do this with vanilla Rust using map_err, in the real world, most of us reach for the anyhow crate for application-level error handling because it makes chaining context a breeze.
The problem with "naked" errors
Let's start with a simple scenario. I'm building a small tool that reads a user's settings from a file. Here is how I'd write it if I were being lazy:
use std::fs;
use anyhow::Result;
fn load_settings() -> Result<String> {
let content = fs::read_to_string("settings.toml")?;
Ok(content)
}
fn main() {
if let Err(e) = load_settings() {
eprintln!("Error: {}", e);
}
}
If settings.toml is missing, the output is: Error: No such file or directory (os error 2). It's accurate, sure, but it's cold. If this were a massive system, I'd be hunting through the source code just to find which function triggered that IO error.
Wrapping errors in meaning
This is where anyhow::Context comes in. Instead of just passing the error up the stack with ?, we can attach a human-readable string to it. This doesn't replace the original error; it wraps it, creating a chain.
use std::fs;
use anyhow::{Context, Result};
fn load_settings() -> Result<String> {
let path = "settings.toml";
fs::read_to_string(path)
.with_context(|| format!("Failed to read settings file at {}", path))?
}
I used with_context here instead of context because it takes a closure. This is a performance win—the string formatting only happens if an error actually occurs, rather than every time the function is called. Now, if the file is missing, we get: Error: Failed to read settings file at settings.toml: No such file or directory (os error 2).
Where I usually trip up: The "Context Gap"
I'll be honest: when I first started using this pattern, I'd often add context at the very bottom of the call stack and assume that was enough. But as the app grows, you realize you need context at every layer of the operation to truly trace the failure.
Look at this mistake I made while extending the example. I added a helper function to validate the settings, but I forgot to add context to the call site in the main coordinator function:
fn validate_settings(content: &str) -> Result<()> {
if content.is_empty() {
anyhow::bail!("Settings file is empty");
}
Ok(())
}
fn initialize_app() -> Result<()> {
// I added context here...
let settings = load_settings().context("App initialization failed")?;
// ...but I forgot it here!
validate_settings(&settings)?;
Ok(())
}
If validate_settings fails, the error message is just "Settings file is empty". The "App initialization failed" context is completely bypassed because it was only attached to the load_settings call. I've created a "context gap."
Creating a complete diagnostic chain
To fix this, I need to treat error context like a breadcrumb trail. Every time I call a function that could fail, I should ask: "If this fails, what additional information does the user need to know to fix it?"
Here is the refined version. Notice how the error flows from the specific (the file path) to the general (the app initialization):
fn initialize_app() -> Result<()> {
let settings = load_settings()
.context("Could not load initial configuration")?;
validate_settings(&settings)
.context("Configuration was loaded but contained invalid data")?;
Ok(())
}
Now, if the file is empty, the output becomes a beautiful, hierarchical trace:
Error: Configuration was loaded but contained invalid data: Settings file is empty
By chaining these, you've turned a cryptic crash into a diagnostic report. You aren't just telling the user that it failed; you're telling them where in the logic the failure happened.
📋 Practical Task
Build a Multi-Stage Asset Loader with Error Chaining
Create a program that simulates loading a game's assets. You need to implement a chain of functions that can fail, ensuring that every failure provides a clear "breadcrumb" of context using anyhow.
- Create a function
load_map_file()that attempts to read a file namedmap.txt. Give it context including the filename. - Create a function
parse_map_data()that takes the string from the map file and fails if the string doesn't contain the word "START". Give it context explaining that the map format is invalid. - Create a coordinator function
setup_game_world()that calls both of the above. Wrap each call in its own context (e.g., "Failed to initialize game world"). - In
main, callsetup_game_world()and print the resulting error using{:?}(the debug formatter) to see the full chain of causes.
Goal: When map.txt is missing or invalid, your output should show at least three layers of context: the high-level "game world" failure, the mid-level "map loading/parsing" failure, and the low-level IO or validation error.
There are no comments for now.