November 7, 2019·6 min read·engineeringrustasyncfutureslanguage

Rust 1.39 makes asynchronous suspension part of ordinary function code

Stable async functions return suspended futures, and await drives their progress within another asynchronous computation. The syntax improves composition without choosing a runtime or scheduling work merely because a function was called.

Calling an asynchronous function in Rust does not schedule it to run. It creates a future representing suspended work. That distinction is essential to understanding the async and await syntax stabilized in Rust 1.39, because sequential-looking source still depends on the polling model that drives the computation.

The November 7 release announcement stabilizes async functions and blocks together with the .await operator. Niko Matsakis's companion explanation connects the syntax to the existing future model and describes this first stable version as a minimum viable product. It is a language milestone with further implementation and diagnostic work still expected.

I find Rust's asynchronous model interesting because the cost and control of execution remain explicit. The new syntax can make control flow easier to follow without pretending that creating a future is the same operation as starting an independently scheduled task. That distinction matters when composing library code into a larger system.

An async call returns the computation

An ordinary function call executes its body before returning. Calling an async fn instead returns a future. Its declared return type describes the future's eventual output, rather than a value immediately produced by the call.

For example, these functions use the stable syntax in a Rust 2018 crate. They define asynchronous computations without selecting an executor to run them.

async fn read_count() -> Result<u32, &'static str> {
    Ok(7)
}

async fn doubled_count() -> Result<u32, &'static str> {
    let count = read_count().await?;
    Ok(count * 2)
}

The example performs no actual I/O. Its purpose is to show the relationship between the call, the awaited output and error propagation. Creating the future returned by doubled_count() alone does not execute the body to completion. Something must drive that future.

Inside the second function, read_count().await produces the first future's output when it completes. The following ? handles the resulting Result, propagating an error through the surrounding asynchronous function's result. These operations occur in sequence even though the syntax places them in one expression.

Matsakis explains the postfix choice as a way to combine await with ? and method chains. The expression's order remains visible, first obtain the future's output, then perform the next operation on that output. This reduces the syntactic work required to combine waiting with ordinary Rust error handling.

Suspension returns control to the runtime

An await point can suspend the enclosing computation when the awaited future cannot yet complete. Control returns to the runtime, allowing other work to proceed. When the computation can make progress, it can resume from the suspended point.

That behavior is different from blocking an operating-system thread until an I/O operation finishes. The syntax gives the compiler enough structure to represent suspended work as a future. It does not make arbitrary blocking operations nonblocking merely because they appear inside an async function.

The Future interface stabilized in July supplies the foundation. Its versioned definition uses a pinned mutable receiver and a task context. Polling returns either a completed output or Pending, with a waker arranging notification when another poll may be useful.

This explains the division of responsibility. The language syntax represents a computation that can suspend. Futures and task notification express progress. A runtime or executor supplies the machinery that chooses tasks and polls them. Stabilizing the syntax does not select one runtime as part of every Rust program.

The future documentation also says polling should return promptly. A future that performs long blocking work inside a poll can obstruct the thread driving other tasks. Await syntax expresses where suspension is possible, but the underlying operation still has to cooperate with the polling contract for that concurrency to work as intended.

Wakeups do not promise completion either. They tell the driver to attempt another poll, which can still return Pending. The distinction keeps readiness notification separate from the final result. A runtime can schedule another attempt without the caller assuming that one notification necessarily supplies the operation's output.

Composition does not schedule every nested future separately

Matsakis contrasts Rust's polling model with systems where calling an asynchronous function schedules it immediately. Rust can compose nested futures without requiring a separate scheduling action for each call. The resulting future represents the composition, and its driver advances the work through polling.

This is the mechanism behind the article's discussion of zero-cost futures. It should not be read as a claim that asynchronous I/O consumes no memory or processor time. The specific avoided requirement is immediate scheduling work for every future created by an async call.

The model also explains why storing a newly created future is different from spawning a task. A program may create a future and await it later, or compose it with other futures. Independent scheduling requires an operation supplied by the chosen execution environment, rather than arising automatically from the function call.

I prefer keeping this distinction in API discussions because it prevents an easy category error. A function that returns a future has described work. A runtime call that schedules a task has made an execution decision. Those can be combined, but treating them as interchangeable makes it harder to explain when resources are used.

Async blocks cover local computations

The release includes async { ... } and async move { ... } blocks alongside named functions. The announcement describes them as closure-like ways to define asynchronous literals. They let source express a local asynchronous computation without requiring a separate named function for every piece of work.

The resulting value is still a future. Putting code in an async block does not by itself grant it an execution thread or arrange polling. The same creation-versus-execution distinction applies whether the computation came from a named function or a block.

This is useful for describing the stable feature's scope. The release adds syntax for expressing and awaiting suspended computations. It does not promise that every existing library operation has an asynchronous counterpart, or that every higher-level application framework already supports the new interfaces.

The ecosystem is adopting the standard interface

The companion post reports that futures-rs 0.3.0 supports async and await, and that Hyper has migrated to standard Rust futures. It also describes work underway in Tokio and async-std. Their upcoming releases are plans in this account, rather than completed events that should be attributed to today's compiler release.

This staged adoption is part of why the July stabilization mattered. A stable Future trait let libraries prepare a shared interface before the language syntax arrived. Compiler support and ecosystem integration can then advance on related schedules without requiring one release to contain every application-level component.

The post also identifies diagnostic improvements since beta and continued work in the Async Foundations working group. A stable feature can still have usability problems worth addressing. The minimum viable product description preserves that distinction without reducing the meaning of syntax being available on stable Rust today.

Borrow checking changes have a separate release boundary

Rust 1.39 also turns borrow-checker migration warnings into errors for the 2018 edition. The release explains that the old checker accepted some code with memory-safety problems, while non-lexical lifetimes corrected those cases. Migration mode had temporarily warned when old and new checkers disagreed in this direction.

The warning-to-error change in this release applies to Rust 2018. Extending it to Rust 2015 is announced for the following release. That timing matters for an existing codebase, because selecting an edition and selecting a compiler version together determine how those previous warnings appear.

This is a separate compatibility change from async syntax, despite their shared release. An upgrade can expose a borrow-checking error in code that uses no async functions. Keeping the changes distinct helps explain why a compilation failure should be traced to its diagnostic instead of attributed to the release's headline feature.

The call site still defines the sequence

An awaited operation reads much like an ordinary call while retaining a visible suspension point. That makes a sequence of dependent operations easier to express than an equivalent chain of manually composed continuations. Matsakis also emphasizes improved integration with Rust's borrowing system compared with earlier combinator-heavy approaches.

The practical benefit is a source representation of work that can pause and resume without requiring the caller to describe every transition manually. Its execution still follows the future contract. An async call creates the suspended computation, an await point participates in driving it, and the chosen runtime supplies the outer polling and scheduling decisions.