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
108: The ? Operator Internals
You've probably used the ? operator a hundred times by now. It feels like magic: you slap it on the end of a Result, and if it's an error, the function just exits early. But if you treat it as a black box, you're going to hit a wall the moment your project grows beyond a single module.
Take a look at this snippet. I'm trying to read a number from a file and return it. Simple enough, right?
fn read_port_from_file() -> std::io::Result<u16> {
let content = std::fs::read_to_string("port.txt")?;
let port: u16 = content.trim().parse()?;
Ok(port)
}
The mismatched type wall
If you try to compile this, the compiler is going to yell at you. It'll tell you that std::num::ParseIntError cannot be converted to std::io::Error. This is the exact moment most learners get frustrated because they think they're doing something wrong with the ? operator itself.
Here is the secret: ? isn't just a "return if error" shortcut. It actually performs an implicit type conversion. Under the hood, the compiler expands expr? into something that looks roughly like this:
match expr {
Ok(val) => val,
Err(err) => return Err(std::convert::From::from(err)),
}
Notice that From::from(err) call? That's the key. The ? operator tries to convert the error returned by the expression into the error type defined in your function's return signature. In my broken example, read_to_string returns an io::Error, which matches the function signature, so it works. But parse() returns a ParseIntError. Since Rust doesn't have a built-in implementation of From<ParseIntError> for io::Error>, the code fails to compile.
Unifying errors with a custom enum
To fix this, we need to stop pretending that every error in our function is an IO error. We need a type that can represent any error that might happen in this context. I usually do this by creating a custom error enum.
Here is how I'd refactor that function to make it actually work:
#[derive(Debug)]
enum ConfigError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
// We tell Rust how to turn an io::Error into a ConfigError
impl From<std::io::Error> for ConfigError {
fn from(err: std::io::Error) -> Self {
ConfigError::Io(err)
}
}
// And how to turn a ParseIntError into a ConfigError
impl From<std::num::ParseIntError> for ConfigError {
fn from(err: std::num::ParseIntError) -> Self {
ConfigError::Parse(err)
}
}
fn read_port_from_file() -> Result<u16, ConfigError> {
let content = std::fs::read_to_string("port.txt")?; // Calls ConfigError::from(io_err)
let port: u16 = content.trim().parse()?; // Calls ConfigError::from(parse_err)
Ok(port)
}
Now, when the ? operator hits an error, it looks at the return type (ConfigError) and checks if the error it just caught can be converted into that type using the From trait. Because we implemented From for both possible error types, the "magic" works again.
I'll be honest: writing those From implementations by hand is tedious. In a real-world production codebase, I'd almost certainly use a crate like thiserror to derive those implementations for me. But understanding that ? is just a wrapper around From::from is what separates someone who guesses at their types from someone who actually controls them.
📋 Practical Task
Implementing a Multi-Source Data Loader
You are building a system that loads a user's ID from a file and then validates that ID against a mock database (which might return a custom "NotFound" error). Your goal is to use the ? operator to handle these disparate error types cleanly.
Requirements:
- Define a custom error enum called
LoaderErrorthat can wrap bothstd::io::Errorand a customDatabaseError(just a simple unit struct). - Implement the
Fromtrait forLoaderErrorfor both error types. - Write a function
load_user_id() -> Result<u32, LoaderError>that:- Reads a filename from a string.
- Parses that string into a
u32. - Calls a mock function
check_db(id: u32) -> Result<(), DatabaseError>.
- Use the
?operator for all three potentially failing operations.
Starter Code:
struct DatabaseError;
fn check_db(id: u32) -> Result<(), DatabaseError> {
if id == 0 { Err(DatabaseError) } else { Ok(()) }
}
// Your implementation goes here...
There are no comments for now.