🎓 A1Learner
🔍
0 of 45 lessons completed

Rust Async Trait Patterns

An async trait pattern is a full spelling of an async trait method and the future it returns. One pattern writes the method out in full, with no crate.

async fn is short for this

Nothing new happens here. This is the longhand the compiler reads your async fn as. The Send promise is said out loud:

trait Source {
    fn fetch(&self) -> impl Future<Output = String> + Send;
}

Box the future to mix types

One return type fits every impl, so trait objects work. The + '_ promises the future may borrow self:

trait AnySource {
    fn fetch_boxed(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}

impl AnySource for Api {
    fn fetch_boxed(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>> {
        Box::pin(async { String::from("from the api") })
    }
}

A default method on top

Write the shared behaviour once, in the trait, on top of the one method each type provides:

trait Source {
    fn fetch(&self) -> impl Future<Output = String> + Send;
    async fn fetch_loud(&self) -> String {
        self.fetch().await.to_uppercase()
    }
}

The default method comes free

Api never wrote fetch_loud — the trait's default body runs on top of its own fetch:

let api = Api;
println!("{}", api.fetch_loud().await);

Example

// Cargo.toml: tokio = { version = "1", features = ["full"] }
use std::future::Future;
use std::pin::Pin;

trait Source {
    // Written out in full: each impl returns its own future type.
    fn fetch(&self) -> impl Future<Output = String> + Send;
    // A default method, built on fetch:
    async fn fetch_loud(&self) -> String {
        self.fetch().await.to_uppercase()
    }
}

struct Api;
impl Source for Api {
    async fn fetch(&self) -> String {
        String::from("from the api")
    }
}

struct Cache;
impl Source for Cache {
    async fn fetch(&self) -> String {
        String::from("from the cache")
    }
}

// The dyn-friendly twin: one boxed future type fits every impl.
trait AnySource {
    fn fetch_boxed(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}

impl AnySource for Api {
    fn fetch_boxed(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>> {
        Box::pin(async { String::from("from the api") })
    }
}
impl AnySource for Cache {
    fn fetch_boxed(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>> {
        Box::pin(async { String::from("from the cache") })
    }
}

#[tokio::main]
async fn main() {
    let api = Api;
    println!("{}", api.fetch().await);
    println!("{}", api.fetch_loud().await);

    let sources: Vec<Box<dyn AnySource>> = vec![Box::new(Api), Box::new(Cache)];
    for s in &sources {
        println!("boxed: {}", s.fetch_boxed().await);
    }
}

Key points

  • The written-out shape still cannot be dyn.
  • Each type returns a different future type.
  • One Vec still cannot hold different sources this way
  • With the boxed shape, Vec<Box<dyn AnySource>> works.
  • Different types, one collection, one calling style.
  • The price is one heap allocation per call
  • The async_trait crate writes the boxed shape for you.
  • Hand-writing it is the same trade with no dependency, and you control the Send and lifetime bounds exactly
  • Rule of thumb: with a fixed set of types, use generics or the written-out shape.
  • When types must mix in one collection, use the boxed shape or async_trait

In this example, two sources — Api and Cache — fetch a line of text each. The Source trait writes fetch out in full. Each impl keeps its own future type. The trait adds a default method, fetch_loud. It shouts whatever fetch returns. Api gets it for free. Then the AnySource trait shows the boxed twin. Both impls return the same Pin<Box<dyn Future>>. So a Vec can hold an Api and a Cache side by side as Box<dyn AnySource>. And one loop fetches from both. Same promises, two shapes — picked by whether the types must mix.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

You must store an Api and a Cache together in one Vec and call fetch on each. Which shape fits?