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
107: Result Combinators: map_err, ok, and_then
A few years ago, I was reviewing a PR from a junior dev who was building a configuration loader. He had this massive function that read a file, parsed it as JSON, and then validated the fields. The logic itself was sound, but the code was a nightmare to read. He had used nested match statements to handle every single possible error. It looked like a staircase—the "pyramid of doom"—where the actual business logic was pushed all the way to the right side of the screen. He was manually converting std::io::Error into his own ConfigError type inside every single arm. It worked, but it was fragile and exhausting to maintain.
That's where Result combinators come in. Instead of treating every Result as a fork in the road that requires a match, you can treat them as a pipeline. You're essentially telling Rust: "If this succeeded, do the next thing; if it failed, transform the error and keep moving."
Transforming Failures with map_err
One of the most common frictions in Rust is dealing with different error types. Your database driver returns one error, your JSON parser returns another, and your own domain logic returns a third. You can't just return all of them from one function unless you're using a boxed trait object or a heavy enum.
map_err is your best friend here. It allows you to change the Err variant of a Result while leaving the Ok variant completely untouched. I use this constantly to "lift" low-level errors into high-level application errors.
enum AppError {
IoError(String),
ParseError(String),
}
fn read_config() -> Result<String, AppError> {
std::fs::read_to_string("config.toml")
.map_err(|e| AppError::IoError(e.to_string()))
}
Notice how we didn't need a match. If read_to_string returns an Ok, map_err does nothing. If it returns an Err, it runs the closure and wraps the IO error in our AppError.
Chaining Fallible Operations with and_then
Now, what happens when you have a sequence of operations where each one could fail? If you use map to call a function that also returns a Result, you end up with a Result<Result<T, E>, E>. It's a mess to unwrap.
and_then (often called "flatmap" in other languages) solves this. It takes the value inside the Ok and passes it to a closure that returns a new Result. If the first operation fails, the second one is skipped entirely, and the original error is passed along.
fn parse_value(input: String) -> Result<i32, AppError> {
input.trim().parse<i32>()
.map_err(|_| AppError::ParseError("Not a number".to_string()))
}
fn get_and_parse_config() -> Result<i32, AppError> {
read_config() // Returns Result<String, AppError>
.and_then(|content| parse_value(content)) // Returns Result<i32, AppError>
}
This reads like a recipe: "Read the config, and then parse the value." It's linear, clean, and avoids the nesting that plagued that PR I mentioned earlier.
Simplifying Results into Options with ok()
Sometimes, you just don't care why something failed. Maybe you're checking if a cache entry exists; if it's missing or the disk read failed, the outcome is the same: you just go fetch it from the network. In these cases, carrying around a detailed error type is just overhead.
The ok() method converts a Result<T, E> into an Option<T>. It transforms Ok(v) into Some(v) and discards the Err(e) entirely, turning it into None.
let config_val = read_config().ok();
// Now we have an Option<String> instead of a Result.
// We can use .unwrap_or() or other Option methods.
I'll give you a word of caution here: don't over-use ok(). It's tempting to use it just to silence the compiler's warnings about unused results, but you're throwing away potentially critical debugging information. Use it only when the fact of failure is more important than the reason for failure.
📋 Practical Task
Refactoring the User Permission Pipeline
You are working on a system that checks if a user has permission to access a resource. The current implementation is using nested match statements and is very difficult to read. Your task is to refactor the check_access function to use map_err and and_then.
Requirements:
- Convert the
std::io::Errorfromfetch_user_roleinto anAccessError::SystemErrorusingmap_err. - Chain the
fetch_user_roleandvalidate_rolecalls usingand_then. - Ensure the final return type is
Result<bool, AccessError>.
#[derive(Debug, PartialEq)]
enum AccessError {
SystemError(String),
InvalidRole,
}
fn fetch_user_role(user_id: u32) -> Result<String, std::io::Error> {
// Simulating a database read
if user_id == 1 {
Ok("Admin".to_string())
} else {
Err(std::io::Error::new(std::io::ErrorKind::NotFound, "User not found"))
}
}
fn validate_role(role: String) -> Result<bool, AccessError> {
if role == "Admin" {
Ok(true)
} else {
Err(AccessError::InvalidRole)
}
}
// REFACTOR THIS FUNCTION
fn check_access(user_id: u32) -> Result<bool, AccessError> {
match fetch_user_role(user_id) {
Ok(role) => {
match validate_role(role) {
Ok(allowed) => Ok(allowed),
Err(e) => Err(e),
}
}
Err(e) => Err(AccessError::SystemError(e.to_string())),
}
}
fn main() {
assert_eq!(check_access(1), Ok(true));
assert_eq!(check_access(2), Err(AccessError::SystemError("User not found".to_string())));
println!("Refactor successful!");
}There are no comments for now.