TL;DR
Rust's stable compiler won't let you write async fn directly inside a trait definition. The quickest fix: add the async-trait crate and slap #[async_trait] on both the trait and every impl block.
# Cargo.toml
[dependencies]
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }
use async_trait::async_trait;
#[async_trait]
trait Fetcher {
async fn fetch(&self, url: &str) -> String;
}
struct HttpFetcher;
#[async_trait]
impl Fetcher for HttpFetcher {
async fn fetch(&self, url: &str) -> String {
format!("fetched: {}", url)
}
}
What Triggers This Error
Write an async method inside a trait and the compiler hits the brakes:
trait Fetcher {
async fn fetch(&self, url: &str) -> String; // error[E0706]
}
Full error output:
error[E0706]: functions in traits cannot be declared async
--> src/main.rs:2:5
|
2 | async fn fetch(&self, url: &str) -> String;
| -----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| `async` because of this
|
= note: consider using the `async-trait` crate: https://crates.io/crates/async-trait
= note: see issue #91611 <https://github.com/rust-lang/rust/issues/91611> for more information
Here's why it happens. When you write async fn, Rust generates an anonymous impl Future as the return type behind the scenes. In a trait, that anonymous type is different for every implementation โ and stable Rust has no way to express that in the trait signature. This ties back to object safety rules for dyn Trait. The feature tracking this (RPITIT โ Return Position Impl Trait In Traits) took years to land and only hit stable in Rust 1.75.
Fix 1: Use the async-trait Crate (Works in Almost Every Case)
Axum, Tower, and most of the async Rust ecosystem use this approach. It's battle-tested.
Step 1: Add the dependency:
cargo add async-trait
Step 2: Import it and annotate both the trait and every impl block:
use async_trait::async_trait;
#[async_trait]
trait Database {
async fn query(&self, sql: &str) -> Vec<String>;
async fn execute(&self, sql: &str) -> bool;
}
struct Postgres;
#[async_trait]
impl Database for Postgres {
async fn query(&self, sql: &str) -> Vec<String> {
// real implementation here
vec![sql.to_string()]
}
async fn execute(&self, sql: &str) -> bool {
true
}
}
What's happening internally: async-trait rewrites your async methods to return Pin<Box<dyn Future + Send>>. That heap allocation is what makes it work with dyn Trait. There's a small runtime cost, but for most applications it's negligible.
Works with dyn Trait Too
use async_trait::async_trait;
#[async_trait]
trait Notifier {
async fn notify(&self, message: &str);
}
struct EmailNotifier;
#[async_trait]
impl Notifier for EmailNotifier {
async fn notify(&self, message: &str) {
println!("Email: {}", message);
}
}
async fn send_notification(notifier: &dyn Notifier, msg: &str) {
notifier.notify(msg).await;
}
Fix 2: Return impl Future Manually (Zero Dependencies)
Don't want an extra dependency? Don't need dyn Trait? Return an explicit impl Future instead. This landed on stable in Rust 1.75 as part of the RPITIT stabilization.
use std::future::Future;
trait Fetcher {
fn fetch(&self, url: String) -> impl Future<Output = String> + Send;
}
struct HttpFetcher;
impl Fetcher for HttpFetcher {
fn fetch(&self, url: String) -> impl Future<Output = String> + Send {
async move { format!("fetched: {}", url) }
}
}
One hard limit: you cannot use this trait as dyn Fetcher. It only works with static dispatch through generics. If you need runtime polymorphism, go back to Fix 1.
Fix 3: Nightly โ Native async fn in Traits
On nightly Rust, a feature flag lets you write async trait methods directly without any crate:
#![feature(async_fn_in_trait)]
trait Fetcher {
async fn fetch(&self, url: &str) -> String;
}
Skip this for production code. Nightly features break between compiler versions with no warning. Use Fix 1 or Fix 2 on stable.
Which Fix Should You Use?
- Need
dyn Trait(runtime dispatch)? โasync-traitcrate (Fix 1) - Static dispatch only, Rust 1.75+? โ
impl Futurein trait (Fix 2) โ no extra deps - Experimenting on nightly? โ Native
async fn in traitwith the feature flag (Fix 3)
Verifying the Fix
Run a clean build:
cargo build
# Expected: no errors, compiled successfully
For a faster sanity check that skips codegen:
cargo check
Run your async tests with Tokio:
cargo test
Common Mistake: Forgetting #[async_trait] on impl Blocks
After adding async-trait, annotating only the trait definition and forgetting the impl blocks is the #1 follow-up error:
// Wrong โ impl block is missing the attribute
#[async_trait]
trait Worker {
async fn run(&self);
}
struct MyWorker;
impl Worker for MyWorker { // error!
async fn run(&self) {}
}
Every single impl block โ not just the trait โ needs #[async_trait]. Miss one and the compiler will remind you.
Further Reading
- async-trait crate on crates.io
- Rust issue #91611 โ tracking stabilization of async fn in traits
- Rust 1.75 release notes โ RPITIT stabilization

