async-trait (Rust): Async Fns in Traits Pre-1.75

Max WellsMax WellsFounder of Rustify

TL;DR: Before Rust 1.75, you could not write async fn in trait definitions; each impl would return a different anonymous future type, making traits non-object-safe. The async-trait crate solves this by rewriting async fn to return Pin<Box<dyn Future>>. Since Rust 1.75, async fn in traits is stable natively for non-object-safe traits. Use async-trait when you need dyn Trait (dynamic dispatch) with async methods, or when targeting Rust older than 1.75.


Why Can't You Just Write async fn in a Trait?

Each impl of a trait generates a different concrete future type; the compiler cannot represent "a future that differs per impl" in a single trait definition without boxing.

// This is a simplified version of what the compiler sees:
trait Fetcher {
    // What should the return type be? It differs per impl!
    async fn fetch(&self) -> String;
    // = fn fetch(&self) -> impl Future<Output = String>
    // But "impl Future" in a trait position means a different type per impl
}

The async-trait crate works around this by boxing the future:

// What async-trait rewrites your trait to (approximately):
trait Fetcher {
    fn fetch(&self) -> Pin<Box<dyn Future<Output = String> + Send + '_>>;
}

How Do You Use async-trait?

Add #[async_trait] to both the trait definition and every impl block.

[dependencies]
async-trait = "0.1"
use async_trait::async_trait;
 
#[async_trait]
trait Database {
    async fn find_user(&self, id: u64) -> Option<String>;
    async fn save_user(&self, name: &str) -> Result<u64, sqlx::Error>;
}
 
struct PgDatabase {
    pool: sqlx::PgPool,
}
 
#[async_trait]
impl Database for PgDatabase {
    async fn find_user(&self, id: u64) -> Option<String> {
        sqlx::query_scalar!("SELECT name FROM users WHERE id = $1", id as i64)
            .fetch_optional(&self.pool)
            .await
            .ok()
            .flatten()
    }
 
    async fn save_user(&self, name: &str) -> Result<u64, sqlx::Error> {
        sqlx::query_scalar!("INSERT INTO users (name) VALUES ($1) RETURNING id", name)
            .fetch_one(&self.pool)
            .await
            .map(|id| id as u64)
    }
}

How Do You Use dyn Trait With Async Methods?

With async-trait, trait objects (Box<dyn Trait>) work with async methods; this is the main reason to keep using async-trait even on Rust 1.75+.

use async_trait::async_trait;
 
#[async_trait]
trait Notifier: Send + Sync {
    async fn send(&self, message: &str) -> Result<(), String>;
}
 
struct EmailNotifier;
struct SlackNotifier;
 
#[async_trait]
impl Notifier for EmailNotifier {
    async fn send(&self, message: &str) -> Result<(), String> {
        println!("[Email] {message}");
        Ok(())
    }
}
 
#[async_trait]
impl Notifier for SlackNotifier {
    async fn send(&self, message: &str) -> Result<(), String> {
        println!("[Slack] {message}");
        Ok(())
    }
}
 
async fn broadcast(notifiers: &[Box<dyn Notifier>], msg: &str) {
    for n in notifiers {
        n.send(msg).await.unwrap();
    }
}

What Changed in Rust 1.75? (Native Async Fn in Traits)

Rust 1.75 (stable December 2023) introduced native async fn in traits for static dispatch; but dyn Trait with async methods still requires async-trait.

// Rust 1.75+; no async-trait needed for static dispatch
trait Processor {
    async fn process(&self, input: &str) -> String;
}
 
struct MyProcessor;
 
impl Processor for MyProcessor {
    async fn process(&self, input: &str) -> String {
        format!("processed: {input}")
    }
}
 
// Works with generics (static dispatch):
async fn run<P: Processor>(p: &P) {
    let result = p.process("hello").await;
    println!("{result}");
}
 
// Still needs async-trait for dynamic dispatch:
// async fn run_dyn(p: &dyn Processor) { ... }  // ❌ not yet stable

When Should You Use async-trait in 2025?

Scenarioasync-trait needed?
impl Trait for Struct (no dyn) on Rust 1.75+❌ Not needed
Box<dyn Trait> with async methods✅ Use async-trait
Supporting Rust < 1.75✅ Use async-trait
Axum handlers / tower Service trait❌ Not needed (they use different approach)

Frequently Asked Questions

async-trait boxes the future on every call; one heap allocation per async method call. Native async fn in traits avoids this. For hot paths, this matters; for typical web/application code, it is negligible.

Yes; both must be annotated. Forgetting one causes a compile error.

async-trait with ?Send (non-Send futures) works in single-threaded environments. Full no_std support is limited.

By default, async-trait requires futures to be Send (for use in multi-threaded async runtimes). Add ?Send to allow non-Send futures, e.g., for single-threaded WASM contexts:

#[async_trait(?Send)]
trait Handler {
    async fn handle(&self);
}

Sources


  • Async/Await: The foundation async-trait extends
  • Trait: async-trait makes traits support async methods
  • dyn Trait: The main use case for async-trait in 2025
  • Tokio: The runtime that executes async trait methods
  • Mockall: Mockall is one of the most common reasons teams reach for async-trait in tests

Keep Reading

Ready to Land a $80-120k Rust Job?