Async & Await in Rust: a full proposal

I’m really excited to announce the culmination of much of our work over the last four months: a pair of RFCs for supporting async & await notation in Rust. This will be very impactful for Rust in the network services space. The change is proposed as two RFCs:

  • RFC #2394: which adds async & await notation to the language.
  • RFC #2395: which moves a part of the futures library into std to support that syntax.

These RFCs will enable basic async & await syntax support with the full range of Rust features - including borrowing across yield points. The rest of this blog post just covers the answers to some anticipated frequently asked questions; for more details see the two RFCs.

FAQs

Why is await written await!()?

We aren’t exactly certain what syntax we want for the await keyword. If something is a future of a Result - as any IO future likely to be - you want to be able to await it and then apply the ? operator to it. But the order of precedence to enable this might seem surprising - await io_future? would await first and ? second, despite ? being lexically more tightly bound than await.

There are a couple of different syntax designs, but to avoid having to settle on one, we propose to start with the most clearly noisy & non-permanent syntax: a compiler built-in like format_args!. This is an application of Stroustrup’s rule.

How are async functions evaluated?

Async functions return immediately when they are called - none of the code in their body is executed. What they return is a future, representing the state machine of their body transitioning from await to await until it finally returns the final value.

There are other designs which you could imagine in which futures eagerly evaluate up to the first await point before returning. We have decided to instead return immediately because we believe the mental model is simpler - you always know that none of the body of the async function will be evaluated until you begin polling the future it returns. For use cases which have a short intiailization step, the async_block! macro in the futures crate will provide a good alternative.

How much of futures is going into libstd?

Only a very small amount, not even the entirety of futures-core. In particular, we are adding:

  • The Future trait (so that async functions can return a Future).
  • The task system & Executor trait (because they’re needed for the definition of the Future trait).

Not even Stream is being added to libstd at this point, much less the higher level APIs like Sink, the channel mechanisms, futures-io, the threadpool executor, etc. Essentially we will provide the core abstraction, but components like event loops, async concurrency primitives, and so on will continue to live in the ecosystem.

So you don’t have to dig into the RFCs, the definition of Future that we are adding is this:

pub trait Future {
    type Item;
    fn poll(self: Pin<Self>, ctx: &mut task::Context) -> Async<Self::Item>;
}

pub enum Async<T> {
    Ready(T),
    Pending,
}

What is a Pin<Self>?

The Future trait defined in this RFC takes self as Pin<Self>, rather than as &mut self. The Pin API was the subject of another RFC earlier this year; it provides a new API which has stronger guarantees about movement than mutable references do. This way, we can guarantee that an async function (which may have internal references into itself) can never move.

I discussed the importance of this change in an earlier blog series, and Ralf Jung has another blog post describing the formal reasoning justifying the soundness of the pin APIs.

What happened to Future’s Error type?

For a long time, many users have asked for us to remove the Error type from Future, because they did had a non-IO use case in which the Error type was inappropriate. We’ve come around to agreeing with them; having the Error type would require that async functions always return a Result, and would not enable non-IO use cases for asynchronicity (such as lazily & concurrently evaluated pure computation).

Futures which wish to have an error channel will instead of an Output which is a Result. Some Try impls will make it possible to continue to use ? inside of poll in those cases, which returns Async<Result<T, E>>. The error handling combinators will exist on an extension trait implemented for all futures of Results. We don’t think there will be a big ergonomics loss from this change, and in contrast it will open up the futures abstraction to many more use cases.

Why not -> impl Future?

An interesting thing about async & await is that an async function has two return types. The inner return type is the type of expressions that you return, whereas the outer return type is the type that the function, when called, actually evaluates to. Every async function evaluates to an anonymous future type; we could have required users to write that they return an impl Future, rather than returning their “inner” type. This would be more similar to languages like C#, which have users return a Task<T>.

The trade off between returning the inner and outer type is discussed at length in the RFCs. In brief, returning impl Future did not interact well with lifetime elision, and the main motivation in languages like C# was to support a kind of polymorphism we do not intend to support. This tipped the scales in favor of the inner return type.