← Debugging Guides

阅读中文版本

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.

advanceddebuggingrustasynctraits

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 -5

Interpretation

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 -10

Interpretation

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 --version

Interpretation

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;
}