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
28: Propagating Errors with the ? Operator
What is the ? operator actually doing under the hood?
If you're coming from a language with try-catch blocks, the ? operator might feel like a magic shortcut for throwing an exception. It isn't. In Rust, it's actually just a very concise way of writing a match statement that returns early.
Imagine you're writing a function to read a username from a local file. Without the ? operator, your code would look something like this:
fn read_username() -> Result<String, std::io::Error> {
let result = std::fs::read_to_string("username.txt");
let content = match result {
Ok(text) => text,
Err(e) => return Err(e), // Return the error early to the caller
};
Ok(content)
}
That's a lot of boilerplate for something we do constantly. The ? operator collapses that entire match block into a single character. When you place ? after a Result, it says: "If this is Ok, give me the value inside. If it's Err, return that error from the current function immediately."
fn read_username() -> Result<String, std::io::Error> {
let content = std::fs::read_to_string("username.txt")?;
Ok(content)
}
It's much cleaner, right? I personally find it makes the "happy path" of the logic stand out while still being explicit about where things can go wrong.
Why can't I use ? in any function I want?
You've probably already hit a compiler error trying to use ? in main or in a helper function that doesn't return a Result. This is because the ? operator is essentially a return statement in disguise.
Because ? might return an Err, the function it's inside must have a return type that is compatible with that error. You can't use ? in a function that returns () (nothing) because the compiler wouldn't know how to "return" the error to the caller.
If you see an error saying the ? operator can only be used in a function that returns Result or Option, check your function signature. You'll need to change it from something like fn do_work() to fn do_work() -> Result<(), MyError>. Even if you don't have a "successful" value to return, returning Ok(()) at the end of the function satisfies the requirement.
Does this work for Option too, or just Result?
It works for both! I've seen a lot of people assume ? is strictly for error handling, but it's actually defined for both Result<T, E> and Option<T>. When used with an Option, it behaves exactly the same way: if the value is Some(v), it unwraps it; if it's None, it returns None from the whole function.
Here is a quick example of using it to navigate a nested data structure:
struct User {
profile: Option<Profile>,
}
struct Profile {
nickname: Option<String>,
}
fn get_nickname(user: User) -> Option<String> {
// If profile is None, return None. If nickname is None, return None.
let nickname = user.profile?.nickname?;
Some(nickname)
}
One critical rule to remember: you cannot mix them in the same function. You can't use ? on a Result and then use ? on an Option in the same block. The return type of the function can only be one or the other. If you need to mix them, you'll have to manually convert the Option into a Result using .ok_or() first.
📋 Practical Task
Exercise: Refactoring the Settings Loader
You have a piece of code that loads a system setting from a file and attempts to parse it into an integer. Currently, it uses verbose match statements. Your task is to refactor the load_setting function to use the ? operator to propagate both I/O errors and parsing errors.
Requirements:
- The function should return
Result<i32, Box<dyn std::error::Error>>to allow for different types of errors (I/O and ParseInt) to be propagated. - Replace the
matchblocks with the?operator. - Ensure the function still returns the parsed integer wrapped in
Ok().
use std::fs;
fn load_setting() -> Result<i32, Box<dyn std::error::Error>> {
// TODO: Refactor the following using the ? operator
let content_result = fs::read_to_string("setting.txt");
let content = match content_result {
Ok(text) => text,
Err(e) => return Err(Box::new(e)),
};
let parsed_result = content.trim().parse<i32>();
let value = match parsed_result {
Ok(num) => num,
Err(e) => return Err(Box::new(e)),
};
Ok(value)
}
fn main() {
match load_setting() {
Ok(val) => println!("Setting loaded: {}", val),
Err(e) => eprintln!("Error: {}", e),
}
}There are no comments for now.