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
220: RAII Guards Pattern
I've seen this exact pattern trip up a lot of developers moving from C++ or Java into Rust. They try to manage resources manually—calling a close(), unlock(), or rollback() method at the end of a function—and then they discover the "early return" problem.
Take a look at this snippet. Imagine we're writing a simple wrapper for a database transaction. We want to make sure that if anything goes wrong, the transaction is rolled back so we don't leave the database in a corrupted state.
struct Transaction {
id: u32,
}
impl Transaction {
fn start(id: u32) -> Self {
println!("Transaction {} started", id);
Self { id }
}
fn commit(self) {
println!("Transaction {} committed to disk", self.id);
}
fn rollback(self) {
println!("Transaction {} rolled back!", self.id);
}
}
fn process_payment(amount: i32) -> Result<(), String> {
let tx = Transaction::start(101);
if amount < 0 {
// Oops! We return early here.
return Err("Negative amount".to_string());
}
// Imagine more complex logic here that could also return Err(...)
tx.commit();
Ok(())
}
The Early Return Leak
If you run process_payment(-10), you'll notice something missing: the rollback never happens. Because we returned early using the return keyword (or the ? operator in a real app), the execution never reaches tx.commit(), and we never called tx.rollback(). The transaction is now "hanging" in the database, potentially holding locks on rows that other users need.
You could manually call tx.rollback() before every single error return, but that's a nightmare to maintain. In a large function with ten different error paths, you'll eventually forget one. That's where the RAII (Resource Acquisition Is Initialization) Guard pattern comes in.
Automating Cleanup with the Drop Trait
In Rust, the "Guard" pattern relies on the Drop trait. Instead of relying on the programmer to remember to clean up, we tie the cleanup logic to the lifetime of the object itself. When the guard goes out of scope—regardless of whether the function finished successfully, returned an error, or even panicked—Rust automatically calls drop().
Here is how we rewrite that transaction logic to be bulletproof:
struct Transaction {
id: u32,
committed: bool, // We need to track if we already committed
}
impl Transaction {
fn start(id: u32) -> Self {
println!("Transaction {} started", id);
Self { id, committed: false }
}
fn commit(mut self) {
println!("Transaction {} committed to disk", self.id);
self.committed = true;
}
}
impl Drop for Transaction {
fn drop(&mut self) {
if !self.committed {
println!("Transaction {} rolled back automatically!", self.id);
}
}
}
fn process_payment(amount: i32) -> Result<(), String> {
let tx = Transaction::start(101);
if amount < 0 {
return Err("Negative amount".to_string());
// tx goes out of scope here, drop() is called, rollback happens!
}
tx.commit();
// tx is consumed by commit(), but because we set committed = true,
// the drop logic knows not to roll back.
Ok(())
}
Why this is the "Rust Way"
The beauty here is that the Transaction struct is now a "Guard." It protects the resource. I personally love this pattern because it moves the responsibility of correctness from the caller (the person writing the business logic) to the type (the person designing the API).
Notice how process_payment is now much cleaner. It doesn't have to care about the failure state of the transaction; it only cares about the "happy path." If the function exits for any reason, the Drop implementation acts as a safety net.
One small detail: I added a committed boolean. This is a common requirement for guards. Since drop is called even after a successful commit(), we need a way to tell the guard, "Hey, everything went fine, you don't need to trigger the emergency cleanup."
📋 Practical Task
Building a Scoped Temporary File Guard
In this exercise, you will implement a TempFileGuard that ensures a temporary file is deleted from the disk when the guard is dropped, unless the user explicitly decides to "persist" the file.
Requirements:
- Create a struct
TempFileGuardthat holds apath: Stringand apersist: bool. - Implement a
new(path: &str)method that prints "Creating temp file at [path]" and returns the guard. - Implement a
persist(&mut self)method that sets thepersistflag totrueand prints "File marked for persistence". - Implement the
Droptrait forTempFileGuard. Ifpersistisfalse, it should print "Deleting temp file at [path]". Iftrue, it should print "Keeping temp file at [path]".
Test your implementation:
- Create a scenario where the guard is created and then the function returns early (simulate an error), verifying the file is deleted.
- Create a scenario where
persist()is called before the guard goes out of scope, verifying the file is kept.
There are no comments for now.