The Rust Async Working Group is excited to announce major progress towards our goal of enabling the use of async fn
in traits. The next Rust release will include stable support for both -> impl Trait
notation and async fn
in traits.
This is a big milestone, and we know many users will be itching to try these out in their own code. However, we are still missing some important features that many users need. Read on for an recommendations on when and how to use the stabilized features.
Ever since RFC #1522, Rust has allowed users to write impl Trait
as the return type of functions. This means that the function returns "some type that implements Trait
". This is commonly used to return iterators, closures, or other types that are complex or even impossible to write explicitly:
Starting in Rust 1.75, which hits stable next week, you can use return-position impl Trait
in traits (RPITIT) and trait impls. For example, you could use this to write a trait that returns an iterator:
So what does all of this have to do with async functions? Well, async functions are "just sugar" for a function that returns -> impl Future
. Since that is now permitted in traits, we also permit you to write traits that use async fn
.
-> impl Trait
in public traitsThe use of -> impl Trait
is still discouraged for general use in public traits, for the reason that users can't use additional bounds on the return type. For example, there is no way to write this function:
In the future we plan to add a solution for this. For now, this feature is best used in internal traits or when you're confident your users won't need additional bounds like this. Otherwise, you should continue using associated types.
async fn
in public traitsSince async fn
desugars to -> impl Future
, the same limitations apply. If you use bare async fn
in a public trait today, you'll see a warning. Thankfully, we have a solution that allows using async fn
in public traits today!
Of particular interest to users of async are Send
bounds on the returned future. Since users cannot add bounds later, the error message is saying that you as a trait author need to make a choice: Do you want your trait to work with multithreaded, work-stealing executors?
For public traits, we recommend using the trait-variant
proc macro to let your users choose. Add it to your project with cargo add trait-variant
, then use it like so:
This creates two versions of your trait: LocalHttpService
for single-threaded executors and HttpService
for multithreaded work-stealing executors. Since we expect the latter to be used more commonly, it has the shorter name in this example. It has additional Send bounds:
This macro works for async because impl Future
rarely requires additional bounds other than Send, so we can set our users up for success. See this blog post for a more thorough explanation of the problem.[1]
Traits that use -> impl Trait
and async fn
are not object-safe, which means they lack support for dynamic dispatch. We plan to provide utilities that enable dynamic dispatch in an upcoming version of the trait-variant
crate.
In the future we would like to allow users to add their own bounds to impl Trait
return types, which would make them more generally useful. It would also enable more advanced uses of async fn
. The syntax might look something like this:
Since these aliases won't require any support on the part of the trait author, it will technically make the Send variants of async traits unnecessary. Those are still a nice convenience for users, so we expect that most crates will continue to provide them.
-> impl Trait
in traits?For private traits you can use -> impl Trait
freely. For public traits, it's best to avoid them for now unless you can anticipate all the bounds your users might want (in which case you can use #[trait_variant]
, as we do for async). We expect to lift this restriction in the future.
#[async_trait]
macro?There are a couple of reasons you might need to continue using async-trait:
As stated above, we hope to enable dynamic dispatch in a future version of trait-variant.
async fn
in traits? What are the limitations?Assuming you don't need to use #[async_trait]
for one of the reasons stated above, it's totally fine to use regular async fn
in traits. Just remember to use #[trait_variant]
if you want to support multithreaded runtimes.
The biggest limitation is that a type must always decide if it implements the Send or non-Send version of a trait. It cannot implement the Send version conditionally on one of its generics. This can come up in the middleware pattern, for example, RequestLimitingService<T>
that is HttpService if T: HttpService
.
#[trait_variant]
and Send bounds?In simple cases you may find that your trait appears to work fine with a multithreaded executor. There are some patterns that just won't work, however. Consider the following:
Without Send bounds on our trait, this would fail to compile with the error: "future cannot be sent between threads safely". By creating a variant of your trait with Send bounds, you avoid sending your users into this trap.
Yes, you can freely move between the async fn
and -> impl Future
spelling in your traits and impls. This is true even when one form has a Send bound.[2] This makes the traits created by trait_variant
nicer to use.
impl Future + '_
?For -> impl Trait
in traits we adopted the 2024 Capture Rules early. This means that the + '_
you often see today is unnecessary in traits, because the return type is already assumed to capture input lifetimes. In the 2024 edition this rule will apply to all function signatures. See the linked RFC for more.
-> impl Trait
?If your impl signature includes more detailed information than the trait itself, you'll get a warning:
The reason is that you may be leaking more details of your implementation than you meant to. For instance, should the following code compile?
Thanks to refined trait implementations it does compile, but the compiler asks you to confirm your intent to refine the trait interface with #[allow(refining_impl_trait)]
on the impl.
The Async Working Group is very happy to end 2023 by announcing the completion of our primary goal for the year! Thank you to everyone who participated in design, implementation, and stabilization discussions. Thanks also to the users of Async Rust who have given great feedback over the years. We're looking forward to seeing what you build, and to delivering continued improvements in the year to come.
Note that we originally said we would solve the Send bound problem before shipping async fn
in traits, but we decided to cut that from the scope and ship the trait-variant
crate instead. ↩︎
This works because of auto-trait leakage, which allows knowledge of auto traits to "leak" outside of an impl signature that does not specify them. ↩︎