Skip to Content
Course content

150: Async Traits and the async-trait Crate

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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:

  1. Define a trait named NotificationSender. It should have one async method: send(&self, recipient: &str, message: &str) -> Result<(), String>.
  2. Create a struct EmailSender and implement NotificationSender for it. In the send method, simply print "Sending Email to [recipient]..." and return Ok(()).
  3. Create a struct MockSender and implement NotificationSender for it. This one should just return Ok(()) without printing anything (to keep test logs clean).
  4. In your main function, create a Vec<Box<dyn NotificationSender>> containing one of each implementation.
  5. Loop through the vector and await the send method for each sender.

Hint: Don't forget to add the #[async_trait] attribute to both the trait and every implementation block.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.