Async Rust in 2023 and beyond

The Async Rust working group has been revisiting our roadmap for the next few years. We'd like to share our plan for what we'll accomplish in 2023, 2024, and beyond.

Scope of the roadmap

First, we have sharpened the scope of the roadmap to only include language and standard library features.

Tooling and compiler-based improvements are still part of our overall vision for async, but their dependency graphs look very different.

We're more confident in our ability to drive this narrower scope of features and deliver them on a given timeline. We think it would also be great to have others step up and drive the other areas.

Tenets

These are the criteria and beliefs that we used to form this roadmap. They are ordered such that, if in conflict, we give priority to the first one.

Steady progress. People are building foundational layers in async Rust now. We need to ship incrementally, building our way to the end goal, but improving stable Rust along the way. This means maintaining a razor-sharp focus, even as we consider how later developments might interact with our current goals.

Stay ambitious. We need to continue supporting the async Rust code that exists today, but we shouldn't be held back by it. The current experience isn't where we want it to be, and we shouldn't be afraid to propose and make ambitious changes, as long as there's a path from here to there.

Enable the ecosystem. The primary role of the Rust org is to build the interconnections that allow the ecosystem to scale and interoperate. Sometimes standardizing one layer simplifies further development atop it, allowing for a flourishing ecosystem.

Drive consistency. The async Rust experience right now varies dramatically depending on your domain, and getting started involves a lot of decisions with non-obvious and long-lasting implications. We need to find ways to make async Rust more uniform.

See also the Rustacean design principles (Reliability, Performant, Supportive, Productive, Transparent, Versatile, in that order). The Async Rust experience as it stands now arguably puts Versatility above Reliability, Supportiveness, and Productivity through constructs like select! and FuturesUnordered as well as the lack of portable libraries and idioms that work uniformly across runtimes.

Overview

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Also available in table form.

Generally speaking, each item in this diagram depends on the items beneath it. Solid lines are for things we know we want, dashed lines represent stepping stones, and dotted lines are for things we aren't sure about yet.

2023: Async fn in trait

This year we will stabilize the MVP for async fn in trait. This includes:

  • async fn in trait
  • -> impl Trait in trait
  • Simple Send bounds

Whatever combination of features we end up with, they must be sufficient for 3 or more representative use cases to publish stable APIs with. Some less-ergonomic workarounds may be required, as long as fixing them won't require breaking APIs later.

  • Customizable providers in AWS SDK
  • Tower service traits
  • Async on embedded with Embassy
  • maybe others

The MVP does not include dynamic dispatch or "complex" cases of Send bounds.

In order to ship this year, we'll need a stabilization PR to land sometime in early Q3. This means we need to be firming up open questions and driving to consensus now.

2024: Async everywhere

By the end of 2024, async will have an equal standing in Rust's most commonly used abstraction mechanisms. This should make async feel like an integrated part of the language, supercharging productivity.

First we want to stabilize an async Iterator trait, including combinators like map. This involves solving real-world problems with async, captures, and generic code, particularly async closures.

Next, round out the async fn and RPITIT stories by making dynamic dispatch work using dyn Trait. This should remove all use cases for the async-trait crate.

Finally, make it possible to publish portable implementations of protocols using official traits for runtime interop and basic I/O. Representative use cases are hyper and quinn.

2024 will be packed, with a lot of exciting possibilties opening up for the ecosystem. If everything goes according to plan this year, we should have most of the second half of 2023 to work on these features, in addition to the first half of 2024.

2027: Everything is awesome

With many of the ecosystem's most fundamental limitations out of the way, we'd like to shift our focus making async Rust more reliable and supportive.

We expect reliability improvements to come mainly through new patterns for managing concurrency ("Combinators 2.0" and structured concurrency), along with a mechanism for async destruction. The fundamental work on abstraction mechanisms like async closures will enable this. It will also be a key thing to look forward to as we work on a Spawn trait in 2024.

We will support async Rust users by making it much easier to get started with a notion of a global executor for use with async fn main, a built-in default runtime, and async APIs for files and sockets in the standard library. We will continue making it possible to customize your runtime while using these features.

Finally, we will deliver further productivity improvements through await patterns (for await). We're also interested in [async] generators and keyword generics, but tend to see those as separate initiatives.

These features are far enough in the future that many details may change. We expect them to come more into focus over the next year or two. Around this time next year (end of Q1 2024) we will add a 2025 milestone and pull some of these features into it.

Alternatives

Reliability in 2024

After shipping fundamental abstraction features like async closures and dyn Trait, we can shift our focus directly to the reliability question and start publishing our new opinionated patterns for use in the broader ecosystem. That would mean de-prioritizing productivity and interop.

In terms of the tenets, this would place ambition and consistency over continuous progress and enabling the ecosystem.

It's true that reliability is one of the most significant problems async Rust faces today, compared to synchronous Rust, and it should be given high priority. It's also likely to be one of the tougher problems to crack, given the diversity of use cases and the many challenges of language design involved. And given its dependence on async closures, there might not be enough iteration time to ship both of these in the same year.

On the other hand, we think we have a pretty good handle on what it would take to ship basic I/O traits. Spawn and timer traits require taking an opinionated stance, but only in a pretty narrow sense of whether we can express a structured concurrency paradigm using them, and that doesn't seem too hard to accommodate. Finally, async Iterator is a clear goal that requires us to exercise our generic programming muscles and may also itself be useful in building them.

There's a chance some of this experimentaion will happen anyway in the meantime. We should do what we can to encourage experimentation with crates like futures_concurrency. One of the ways we'll do that, of course, is by shipping core abstraction mechanisms. We should then revisit this question when planning for 2025.

Generators in 2024

There's been some recent debate over whether async fn next is the right core primitive for async iteration, rather than poll_next. It would be easier to write an async next implementation, but the fact that async functions must return a separate object leads to irregularities in how the async version of the trait would work:

  • You can't store a type implementing async Iterator and the future returned by next() in the same struct, because one borrows from the other.
  • dyn async Iterator always requires boxing or an equivalent adapter.

If we did adopt poll_next we would almost certainly want to prioritize generators and async generators, so these could easily be written. This does have a lot of benefits and reduces the need for combinators.

Alternatively, we could come up an easy way to still write a next()-like implementation as an async function. But it's debated whether this would actually make user's lives much easier, since you are still having to write a state machine by hand.

If we did prioritize async generators, we would have to pick something to deprioritize. Maybe portable protocols, since that's less "fundamental" and we want to keep dyn to finally finish AFIT. Or maybe we bump dyn, because it becomes less important in a world where async Iterator is already dyn-safe.

Tactical planning

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

source: Github project

  • By Apr 1: Prepare write-ups for Send bounds
    • Min RTN (Niko will champion)
    • Min trait transformers (Anyone want to champion?)
    • Noncommittal deriver?
  • By end of Apr:
    • accepted RFC for one of these three approaches
    • plus implemented
  • May
    • …
  • June
    • …
  • By July:
    • stabilized