Async fn in Traits in 2026: What Works, What Doesn’t, and the dyn Workaround
async fn in traits is stable — but dyn still isn’t
Since Rust 1.75, you can write async fn inside a trait definition without reaching for the async-trait crate. For static dispatch — generics, impl Trait parameters, monomorphization — it just works. Rust 1.95, released April 16, 2026, continued tightening compiler diagnostics and ergonomics around it.
The catch is trait objects. Write dyn MyTrait where MyTrait has async methods and the compiler stops you. Async functions in traits are not object-safe, and that remains true in mid-2026.
Why the object safety wall exists
Object safety requires every method to fit into a vtable with a single, uniform slot. Async methods return opaque future types whose sizes differ across concrete implementations. There is no single vtable entry that can accommodate every possible future a concrete type might return — the types simply don’t share a common representation. The language team is working on a native fix via return_type_notation, which would let callers spell out boxed return types inside trait objects explicitly, but as of March 2026 that feature is still unstable.
Three workarounds that actually work today
- Stay with static dispatch. A generic bound
T: MyTraitlets you call async methods with zero ceremony. If you don’t actually need a trait object at runtime, this is almost always the right move — no allocation, no boxing. - The
async-traitcrate. It rewrites your async methods to returnPin<Box<dyn Future>>, which is object-safe. You pay a heap allocation per call. Fine for low-frequency paths; not ideal for tight loops or hot paths. - The
trait-variantcrate. From the rust-lang org, this lets you annotate a trait to auto-generate aSend-bounded variant alongside it. Less invasive than manual boxing when multi-threaded dispatch is the main requirement.
The Send bound problem
Even with static dispatch and multi-threaded Tokio, you can hit cryptic errors when the compiler cannot prove your future is Send. The culprit is usually a non-Send value — a Rc, a raw pointer, or a lock guard held across an await point somewhere in the call chain. The fix is adding explicit + Send bounds on the trait method. Finding exactly where to place them takes practice. Google’s Comprehensive Rust course has a concise section on this pitfall with worked examples.
Tokio 2.0 and the runtime picture
The broader async ecosystem got a meaningful upgrade with Tokio 2.0. The headline change is a new work-stealing scheduler that removes the single shared-queue bottleneck from older releases, distributing tasks across threads with less contention. For IO-heavy network services the throughput improvement is real. TokioConf 2026 covered the scheduler internals in depth — the talk videos were published in May on the official Tokio blog.
Axum, built directly on Tokio, remains the dominant choice for HTTP services. It composes cleanly with the static-dispatch async trait pattern above. The ergonomics gap between Axum and its nearest competitors has widened over the past year.
What’s still on the roadmap
The Rust async team has two milestones left to close the gap with sync Rust: making async traits natively object-safe (via return_type_notation or a successor design), and stabilizing async streams so you can write async fn stream() -> impl Stream<Item = T> inside a trait. Neither has a firm date. Niko Matsakis’s baby steps blog — he’s been running a series on dyn async traits — is the best running commentary on where the design is heading. The RFC tracker is the other place to watch.
Sources
- blog.rust-lang.org
- rust-lang.github.io
- tokio.rs
- github.com
- google.github.io
- smallcultfollowing.com
- rustfoundation.org
