For years, async traits were Rust's most embarrassing hole. You could write async code, but you couldn't put async functions in traits—not reliably, not safely, not without reaching for nightly features or existential type gymnastics.

That finally changed. Here's the story of what broke, what was built to fix it, and what's still hard.

The Original Problem

Say you wanted to write a trait for a web client:

trait HttpClient {
    async fn get(&self, url: &str) -> Result<String, Error>;
}

This looks simple but compiles to nonsense. async fn in a trait desugars to returning a future, and futures are generic. The compiler can't name the future type—this is the "unnameable type" problem.

Early Rust versions simply didn't allow this. You got an error:

error: async fn in trait is unstable

The Workarounds (That We All Used)

1. Return Pin<Box>

Manually box the future:

trait HttpClient {
    fn get(&self, url: &str) -> Pin<Box<dyn Future<Output = Result<String, Error>> + '_>>;
}

This works but has problems:

2. async-trait Crate

The async-trait crate solved this with a macro:

use async_trait::async_trait;

#[async_trait]
trait HttpClient {
    async fn get(&self, url: &str) -> Result<String, Error>;
}

The macro desugars to the boxed version under the hood. It became the de facto standard. Every async Rust project imported it.

But it was still a workaround. A crate shouldn't be necessary for something this fundamental.

3. RPITIT (Return Position Impl Trait in Traits)

Rust 1.75 (December 2023) stabilized RPITIT. This lets you write:

trait HttpClient {
    fn get(&self, url: &str) -> impl Future<Output = Result<String, Error>>;
}

At the call site, the return type is inferred. The compiler generates a unique opaque type for each impl. No boxing required—static dispatch when possible, zero overhead when the compiler can monomorphize.

This was huge. But it only worked for sync functions returning futures.

4. async fn in Traits

Rust 1.75 also stabilized async fn directly in traits:

trait HttpClient {
    async fn get(&self, url: &str) -> Result<String, Error>;
}

Finally. First-class async in traits, no crates needed.

But there were catches.

What's Actually Stable Now (Early 2026)

As of Rust 1.85+ (early 2026), here's what's stable:

What's still catching people:

The Parity Goal

The Rust async team has an explicit goal: "bring async Rust closer to parity with sync Rust." The 2025-2026 roadmap includes:

These are the remaining pieces. Not blockers anymore, but rough edges.

What This Means For You

If you're starting a new project in 2026:

  1. Don't reach for async-trait crate unless you need dyn MyAsyncTrait. The built-in async fn in traits is stable and faster.

  2. Use RPITIT when you need flexibility in return types within traits. It's zero-cost abstraction.

  3. Understand the Send/Sync constraints — test your async trait implementations with tokio::test and assertions about Send bounds.

  4. Watch for async stream — the final piece of the async iteration puzzle is stabilizing. Once it lands, you'll be able to write async fn stream() -> impl Stream<Item = T> in traits.

The Bigger Picture

Async traits went from "impossible" to "stable feature" over about 8 years. The journey shows something about Rust's evolution: they don't rush unstable things into std, they get them right.

The rough edges that remain—auto traits, object safety—are hard problems. They're not being ignored. They're being solved carefully.

If you're writing library code today, you no longer need workarounds. The standard library has your back.