Rust Async Traits
An async trait is a trait with async methods, where a trait is a promise about behaviour. Each async method builds a future when it is called.
An async fn in a trait
The trait promises work that finishes later. Each impl returns its own future, like any async fn:
trait Greeter {
async fn greet(&self) -> String;
}Generic code just works
A function generic over the trait can await the method directly โ no extra machinery:
async fn announce<G: Greeter>(g: &G) {
println!("greeter says: {}", g.greet().await);
}
announce(&Loud).await;But one Vec cannot hold them all
As a trait object, the caller cannot know which future type it is waiting on โ so this fails to compile:
let greeters: Vec<Box<dyn Greeter>> = vec![Box::new(Loud), Box::new(Quiet)];
greeters[0].greet().await; // error[E0038]: the trait Greeter is not dyn compatibleThe async_trait crate rewrites the method
Its attribute turns each async method into one returning a boxed future โ a type whose size is known:
#[async_trait::async_trait]
trait Greeter {
async fn greet(&self) -> String; // becomes: -> Pin<Box<dyn Future<...>>>
}Now different greeters share one Vec
With the futures boxed, Loud and Quiet fit in one collection, and each can be awaited:
let greeters: Vec<Box<dyn Greeter>> = vec![Box::new(Loud), Box::new(Quiet)];
for g in &greeters {
println!("greeter says: {}", g.greet().await);
}Example
// Cargo.toml: tokio = { version = "1", features = ["full"] }
trait Greeter {
async fn greet(&self) -> String;
}
struct Loud;
impl Greeter for Loud {
async fn greet(&self) -> String { String::from("HELLO!") }
}
struct Quiet;
impl Greeter for Quiet {
async fn greet(&self) -> String { String::from("hello") }
}
async fn announce<G: Greeter>(g: &G) {
println!("greeter says: {}", g.greet().await);
}
#[tokio::main]
async fn main() {
announce(&Loud).await;
announce(&Quiet).await;
}Key points
- The first catch is trait objects.
- Each type returns a different future type.
- The caller cannot know that type's size.
- dyn Greeter cannot be awaited as written
- Boxing costs one heap allocation per call.
- That small cost is still a real cost.
- Prefer generics when the set of types is fixed at compile time
- The second catch is lifetimes.
- The future an async trait method returns borrows from its inputs.
- That rules out a few borrowing patterns that plain methods allow
- When the boxed future must travel between threads, it also needs a Send promise.
- The async_trait crate has a spelling for that.
- Rust has supported async traits since version 1.75.
In this example, the Greeter trait promises one async method. Loud and Quiet each implement it with their own greeting. The announce function is generic over G: Greeter, so the compiler builds a version for each type and greet().await works with no boxing at all. Both greeters speak in turn. What this example cannot do is mix Loud and Quiet in one Vec as dyn Greeter. For that, the async_trait crate would rewrite greet. It would hand back a boxed future, as the points describe.
Note
Check the async_trait docs when a spawn refuses a trait object for missing Send.
Try it Yourself
Edit the code, then press Run.
Running is simulated ยท Rust Playground
Exercise
Why can an async trait not be used as a trait object (dyn) without help?