Rust — async fn in Traits and dyn Compatibility (E0038 / E0277)
Untangle async fn in trait errors before Rust 1.75 and dyn Trait incompatibility, understand the return-position impl Trait limitation, and pick the right pattern per version.
Symptoms
- error[E0706]: functions in traits cannot be declared async before Rust 1.75
- error[E0038]: the trait is not dyn-compatible (object safe) because method returns impl Trait / references Self
- Calling the trait through Box<dyn Trait> fails even though the same code works with generic type parameters
Possible causes
- async fn in a trait desugars to a method returning impl Future, which is an unnameable type incompatible with vtable dispatch
- The trait needs runtime polymorphism (dyn), but async methods return opaque types the vtable cannot describe
- Before 1.75 there was no language support at all; after 1.75, async fn in traits works for generics but not for dyn
- A returned Box<dyn Future> from a trait method fights type inference on lifetimes and pinning
Diagnosis steps
Identify which of the two errors you have
cargo check 2>&1 | grep -E "E0706|E0038|E0277" | head -5Interpretation
E0706 is the pre-1.75 syntax gap; E0038 is dyn incompatibility. Same code region, different problems and fixes.
Confirm the dispatch style actually needed
grep -Rn "dyn .*Trait\|Box<dyn" src/ | head -10Interpretation
If every use site is generic, dyn compatibility is unnecessary; if heterogeneous collections exist, you need the boxed-future pattern.
Check the toolchain version
rustc --version && cargo --versionInterpretation
1.75+ unlocks native async fn in traits for the generic case, changing the whole solution space.
Root cause
An async fn returns an anonymous future type sized and laid out by the concrete implementation. Trait-object dispatch requires every method to be callable through a vtable with one signature, and an opaque per-implementation return type cannot fit that slot, so the language either rejected async fn in traits outright (pre-1.75) or rejects it under dyn dispatch (after).
Fix
On 1.75+, use native async fn in traits for generic dispatch and the async_trait crate (or manual BoxPinFuture type aliases) where dyn Trait is required. On older toolchains, use async_trait everywhere. Keep trait methods returning owned data dyn-compatible by boxing explicitly.
Risk note
async_trait heap-allocates every call and dynamic dispatches twice. Reserve it for dyn requirements, use generics elsewhere.
Verify the fix
cargo check passes for both generic and dyn call sites; a minimal heterogeneous collection of implementations dispatches correctly at runtime.
Symptom walkthrough
Two errors from one code region, and the distinction decides everything:
error[E0706]: functions in traits cannot be declared async
error[E0038]: the trait `Fetcher` is not dyn-compatible because
method `fetch` is async
The underlying fact: async fn desugars to fn fetch(&self) -> impl Future, and impl Trait in return position is opaque — each implementation returns a differently-sized anonymous type. A vtable needs one fixed signature, so dyn dispatch cannot host it. Generic dispatch can, because the compiler monomorphizes per concrete type — which is exactly the 1.75 capability boundary: async fn in traits works for generics, not for dyn.
Fix in practice
On 1.75+, split by dispatch need. Generic call sites get native syntax:
trait Fetcher {
async fn fetch(&self, url: &str) -> Vec<u8>; // fine for generics
}
async fn load<F: Fetcher>(f: &F) -> Vec<u8> { f.fetch("...").await }
Heterogeneous dispatch needs the box, either via the crate or hand-rolled:
use async_trait::async_trait;
#[async_trait]
trait FetcherDyn: Send + Sync {
async fn fetch(&self, url: &str) -> Vec<u8>; // returns BoxFuture
}
let fetchers: Vec<Box<dyn FetcherDyn>> = vec![Box::new(Http), Box::new(Mock)];
Hand-rolled equivalent when you want zero dependencies:
type FetchFuture = Pin<Box<dyn Future<Output = Vec<u8>> + Send + '_>>;
trait FetcherDyn {
fn fetch(&self, url: &str) -> FetchFuture;
}