Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Just add async

Metadata
Short titleJust add async
What and whyPatterns that work in sync Rust should work in async Rust — traits, closures, drop, scoped tasks
Point of contactNiko Matsakis
Task ownersBenno 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 fn in traits is stable, but you can’t use &dyn Trait with async methods, and there’s no way to require that an impl returns a Send future.
  • Closures: Async closures are stable, but compiler bugs frequently report invalid Send errors, 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, and Pin.
  • Scoped patterns: Sync Rust has std::thread::scope for borrowing into spawned threads. Async spawn APIs require 'static, forcing Arc everywhere.
  • 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

GoalTimespanWhat and why
Box notation for dyn async trait2026-2027Enable dyn dispatch for async traits via .box notation
Add a Share trait2026A trait that identifies types where cloning creates an alias to the same underlying value, like Arc, Rc, and shared references.
Support move(...) expressions in closures2026Precise control over what closures capture and when, eliminating the need for awkward clone-into-temporary patterns.
Move trait2026-2027Let types opt out of being relocated in memory, encoding immovability as a type property rather than a place property.
Guaranteed destructors2026-2027Explore letting types opt out of mem::forget, enabling patterns like safe scoped spawn for async.
Stabilize the next-generation trait solver2026Replace the existing trait solver with a sound, maintainable implementation that unblocks soundness fixes and async features
Prepare TAIT + RTN for stabilization2026Name 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:

  1. These problems matter. The gaps between sync and async Rust are real and worth fixing.
  2. This direction is right. Closing these gaps so “Just add async” works is the right goal.
  3. 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.