Just add async
| Metadata | |
|---|---|
| Short title | Just add async |
| What and why | Patterns that work in sync Rust should work in async Rust — traits, closures, drop, scoped tasks |
| Point of contact | Niko Matsakis |
| Task owners | Benno Lossin, Takayuki Maeda, lcnr, Nia, Niko Matsakis, Santiago Pastorino, TC, Yoshua Wuyts |
Summary
Writing async Rust should be a natural extension of sync Rust: take your sync code, sprinkle some async and await keywords around in logical places, follow the compiler’s guidance, and wind up with working code.
Motivation
The status quo
The promise of tight tail latency, low memory usage, and high-level reliable code has made Rust, and async Rust in particular, a popular choice for network services. Rust is now widely used at all levels, from superscalars like Amazon and Microsoft, to simple CRUD apps built on FAAS platforms like Lambda or Azure Functions, to low-level networking on embedded devices.
Despite this success, async Rust is widely seen as qualitatively harder than sync Rust. Not just more complex, but a different kind of challenge:
“I feel like there’s a ramp in learning and then there’s a jump and then there’s async over here. And so the goal is to get enough excitement about Rust to where you can jump the chasm of sadness and land on the async Rust side.” (from the Rust Vision Doc interviews)
The problem isn’t async concepts themselves. Developers understand concurrency (well, mostly). The problem is that patterns which work in sync Rust don’t transfer to async:
- Traits:
async fnin traits is stable, but you can’t use&dyn Traitwith async methods, and there’s no way to require that an impl returns aSendfuture. - Closures: Async closures are stable, but compiler bugs frequently report invalid
Senderrors, and the trait limitations above make them hard to use in practice. - Recursion: In sync Rust, recursion just works. In async Rust, it requires arcane signatures with explicit lifetimes,
Box,dyn Future, andPin. - Scoped patterns: Sync Rust has
std::thread::scopefor borrowing into spawned threads. Async spawn APIs require'static, forcingArceverywhere. - Drop: Destructors are sync-only. Resources that need async cleanup (database connections, network sessions) can’t clean up properly in
Drop.
Each issue has workarounds. But the workarounds require knowledge that doesn’t transfer from sync Rust, and the compiler doesn’t guide you to them. The result: developers who would otherwise build a network service in Rust hit these walls and wonder if it’s worth the trouble.
The ecosystem is waiting too. Libraries like Tower remain on 0.x because they can’t express the APIs they need. Tower’s Service trait predates async fn in traits and uses complex workarounds. The maintainers want to ship a cleaner design, but they’re blocked on language features — particularly the ability to define one trait that works with both Send and non-Send futures. So Tower waits, and the middleware ecosystem built on it stays in flux.
Design axioms
-
Sync patterns should transfer. If a pattern works in sync Rust, it should work in async Rust. When async requires something extra, the compiler should guide you there.
-
Server-first, but not server-only. We focus on server and application use cases to ship complete workflows now. But designs should leave space for users with stricter requirements. Features that allocate today can be extended with custom allocators or in-place initialization later. We’re not closing doors, we’re opening the first one.
-
Unblock the ecosystem, enable experimentation. The goal isn’t just language features. It’s enabling libraries like Tower to ship stable APIs, and creating space for exploration of harder problems (in-place initialization, structured concurrency) without blocking on them. Ship end-to-end workflows that work today while leaving room for the designs to evolve.
What we are shooting for
Patterns that work in sync Rust should work in async Rust without requiring workarounds, restructuring, or arcane incantations. When async does require something extra (like explicit boxing for dyn dispatch), the compiler guides you with clear, actionable errors — not walls of opaque type errors.
How we get there
| Goal | Timespan | What and why |
|---|---|---|
| Box notation for dyn async trait | 2026-2027 | Enable dyn dispatch for async traits via .box notation |
Add a Share trait | 2026 | A trait that identifies types where cloning creates an alias to the same underlying value, like Arc, Rc, and shared references. |
Support move(...) expressions in closures | 2026 | Precise control over what closures capture and when, eliminating the need for awkward clone-into-temporary patterns. |
Move trait | 2026-2027 | Let types opt out of being relocated in memory, encoding immovability as a type property rather than a place property. |
| Guaranteed destructors | 2026-2027 | Explore letting types opt out of mem::forget, enabling patterns like safe scoped spawn for async. |
| Stabilize the next-generation trait solver | 2026 | Replace the existing trait solver with a sound, maintainable implementation that unblocks soundness fixes and async features |
| Prepare TAIT + RTN for stabilization | 2026 | Name opaque types and bound async return types so async fn in traits works with Send and dyn |
The 2026 goals are largely independent. RTN enables generic async code and is already RFC’d, waiting on trait solver work. AFIDT / .box notation enables dyn dispatch for async traits. Ergonomic ref-counting addresses closure capture pain that’s amplified by async’s 'static spawn requirements. Immobile types and guaranteed destructors enables scoped spawn and async drop by letting types opt out of being moved or forgotten. RTN and AFIDT share a dependency on the next-generation trait solver, which is being worked on separately.
Frequently asked questions
What am I agreeing to by accepting this theme?
Accepting this roadmap means agreeing that:
- These problems matter. The gaps between sync and async Rust are real and worth fixing.
- This direction is right. Closing these gaps so “Just add async” works is the right goal.
- Server-first is the right prioritization. We ship end-to-end workflows for server/application environments first, with designs that leave space for stricter requirements later.
It does not mean agreeing to specific syntax (like .box) or implementation details. Those will be decided in individual goal RFCs.
What about async iterators / streams?
Async iteration is part of this theme’s vision, but not a 2026 focus. The 2026 goals target the foundational work: getting async fn in traits and closures fully working. That’s enough scope for one year. Exploring streams fully will require those foundations plus guaranteed destructors (because streams interact with structured concurrency and cancellation). Once the 2026 work lands, streams become much more tractable.