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 aFuture
). - The task system &
Executor
trait (because they’re needed for the definition of theFuture
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.