TL;DR: Before Rust 1.75, you could not write
async fnin trait definitions; each impl would return a different anonymous future type, making traits non-object-safe. Theasync-traitcrate solves this by rewritingasync fnto returnPin<Box<dyn Future>>. Since Rust 1.75,async fnin traits is stable natively for non-object-safe traits. Useasync-traitwhen you needdyn 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 stableWhen Should You Use async-trait in 2025?
| Scenario | async-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-trait on crates.io
- async-trait GitHub; dtolnay/async-trait
- Rust Blog; Async fn in Trait (1.75)
Related Glossary Terms
- 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
- Rust Generics and Traits Explained: traits are the foundation async-trait extends
- Rust Lifetimes Deep Dive: async trait methods introduce lifetime complexity

