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
150: Async Traits and the async-trait Crate
You've probably reached a point in your project where you want to abstract some asynchronous behavior. Maybe you're building a data layer and want multiple database backends, or you're creating a plugin system where each plugin needs to perform I/O. Naturally, you'd try to do something like this:
trait UserRepo {
async fn get_user(&self, id: u32) -> Option<User>;
}
struct PostgresRepo;
impl UserRepo for PostgresRepo {
async fn get_user(&self, id: u32) -> Option<User> {
// Imagine some actual SQL call here
Some(User { id, name: "Alice".to_string() })
}
}
When you try to compile this, Rust will hit you with a compiler error. Depending on your toolchain version, it'll either tell you that async fn is not allowed in traits or—if you're on a very recent version—it might compile, but the moment you try to use this trait as a trait object (dyn UserRepo), everything falls apart.
The problem with anonymous Futures
Here is why the compiler is complaining. In Rust, an async fn is essentially syntactic sugar. It transforms your return type into an opaque, anonymous type that implements the Future trait. Because every single async fn generates a unique, unnamed type, the trait system has no way of knowing exactly what that type is across different implementations.
For a trait to work, the compiler needs to know the size and layout of the return type. But since PostgresRepo::get_user and MySqlRepo::get_user return two different anonymous futures, they don't share a common type. This makes the trait "not object safe," meaning you can't use Box<dyn UserRepo> to store different implementations in a list.
Using the async-trait crate to box the future
While Rust is slowly adding native support for this, the industry standard for the foreseeable future is the async-trait crate. It solves the problem by "erasing" the specific type of the future. Instead of returning an anonymous future, it returns a Pin<Box<dyn Future<Output = T>>.
To fix our code, we just need to add the #[async_trait] attribute to both the trait definition and the implementation. I've found that forgetting it on the impl block is the most common mistake people make—the compiler will give you a cryptic error saying the types don't match, even though the code looks identical.
use async_trait::async_trait;
#[async_trait]
trait UserRepo {
async fn get_user(&self, id: u32) -> Option<User>;
}
#[async_trait]
impl UserRepo for PostgresRepo {
async fn get_user(&self, id: u32) -> Option<User> {
Some(User { id, name: "Alice".to_string() })
}
}
The trade-off: Heap allocation
I should be honest with you: this isn't a free lunch. Because async-trait uses Box, every time you call get_user, you're performing a small heap allocation. In 99% of application code—like web servers or CLI tools—this overhead is completely negligible. The developer productivity you gain by having a clean, polymorphic API far outweighs the cost of a few bytes on the heap.
However, if you're writing a high-frequency trading engine or an embedded driver where every nanosecond and byte counts, you might want to avoid this and look into returning impl Future manually (which is significantly more verbose) or using the newer, more limited native async trait features if you don't need dyn support.
📋 Practical Task
Implementing a Mockable Async Notification Service
You are building a notification system that can send alerts via either Email or SMS. To make your business logic testable, you need to abstract the sender into a trait so you can swap a real sender for a mock sender during tests.
Your Task:
- Define a trait named
NotificationSender. It should have oneasyncmethod:send(&self, recipient: &str, message: &str) -> Result<(), String>. - Create a struct
EmailSenderand implementNotificationSenderfor it. In thesendmethod, simply print "Sending Email to [recipient]..." and returnOk(()). - Create a struct
MockSenderand implementNotificationSenderfor it. This one should just returnOk(())without printing anything (to keep test logs clean). - In your
mainfunction, create aVec<Box<dyn NotificationSender>>containing one of each implementation. - Loop through the vector and
awaitthesendmethod for each sender.
Hint: Don't forget to add the #[async_trait] attribute to both the trait and every implementation block.
There are no comments for now.