SuperAPI AI Directory

rust-syntax-async-await

Coding & development

View repository

Updated 10-16 06:18

About this skill

rust-syntax-async-await

The mechanics of async fn, .await, and the Future trait in Rust: how async fn desugars to a state machine returning impl Future, what .await actually does at a suspension point, how AFIT (1.75), RPITIT (1.75), and async closures (1.85) compose with the trait system, why Pin exists, and what kinds of state may or may not be held across an .await.

This skill is executor-agnostic: it covers the language and the trait contract. For runtime specifics (#[tokio::main], spawn, JoinSet, select!) see [[rust-impl-async-tokio]]. For runtime-architecture concepts (poll-vs-push, reactor, structured concurrency) see [[rust-core-async-runtime]]. For async error patterns (? in async fn, JoinError, cancellation safety) see [[rust-errors-async]]. For Pin / Rc / Arc / Box ownership see [[rust-syntax-smart-pointers]].

Deep Pin / Unpin mechanics live in references/pinning.md. Complete API surface lives in references/methods.md. Worked examples live in references/examples.md. Anti-patterns live in references/anti-patterns.md.


When to use this skill

  • User writes async fn foo() -> T { ... } and needs to know what it returns
  • User calls fut.await and asks what .await does
  • User defines an async function in a trait (AFIT, 1.75) or returns impl Trait from a trait method (RPITIT, 1.75)
  • User writes an async closure async || { ... } (1.85) or hits the AsyncFn / AsyncFnMut / AsyncFnOnce traits
  • Compiler says "future cannot be sent between threads safely" or "future is not Send"
  • User wants to hold a MutexGuard, RefCell borrow, Rc, or any !Send value across .await
  • User implements Future by hand and asks about Pin<&mut Self> in poll
  • User asks how to cancel a future (answer: drop it)
  • User hits "cannot move out of pinned" or wants to use pin! / Box::pin
  • User asks why their async fn signature requires + use<> in edition 2024

For tokio::spawn, JoinSet, select!, channels, timers and concrete runtime setup see [[rust-impl-async-tokio]].


Quick reference table

Concept Syntax Compiler semantics
Async function async fn f() -> T Returns impl Future<Output = T>; body is a state machine
Async block async { expr } Same as async fn but anonymous; captures by reference unless async move
Async closure (1.85) async |x| { ... } Implements AsyncFn / AsyncFnMut / AsyncFnOnce
Await fut.await Yields control to the executor; resumes when Future::poll returns Ready
AFIT (1.75) trait T { async fn m(&self); } Desugars to fn m(&self) -> impl Future + use<'_, Self>
RPITIT (1.75) trait T { fn m(&self) -> impl Future<...>; } Anonymous return type captured by the impl
Manual Future impl Future for S { fn poll(self: Pin<&mut Self>, cx) -> Poll<_> } Must be Pinned to be polled
pin! macro (1.68) let mut fut = pin!(some_future); Stack-pin a future without Box::pin
Cancellation drop(fut) Runs destructors of locals at the current suspension point
Edition 2024 RPIT fn f<'a>() -> impl Future + use<'a> Default capture is "all in scope generics & lifetimes"

async fn desugaring

async fn fetch(url: &str) -> Result<String, std::io::Error> {
    /* body */
    Ok(String::new())
}

Desugars (conceptually) to:

fn fetch<'a>(url: &'a str) -> impl Future<Output = Result<String, std::io::Error>> + 'a {
    async move {
        /* body */
        Ok(String::new())
    }
}

Three load-bearing facts:

  1. The return type is anonymous: it is impl Future<Output = T>, never a nameable type. You cannot write fn returns_future() -> ConcreteFutureName.
  2. The body is a compiler-generated state machine. Every .await becomes a suspension point with its own state-machine variant; locals live for at most one state.
  3. The state machine is self-referential in general: a local that borrows another local across an .await produces a future whose internal references point into its own storage. This is why Future requires Pin<&mut Self> in poll.

Source: Async Book ch. 1, 2 Source: std::future::Future

ALWAYS treat the return type as opaque: hand it off to an executor or .await it. NEVER try to spell the anonymous future type; use impl Future<Output = T> (or Pin<Box<dyn Future<Output = T> + Send>> for type erasure).


.await mechanics

fut.await desugars to a loop that repeatedly calls Future::poll until it returns Poll::Ready. From the standard library:

"When using a future, you generally won't call poll directly, but instead .await the value." Source: std::future::Future

At a suspension point (.await that returned Pending), the runtime takes back control. The future will be re-polled later, but only after some Waker has been signalled. From the contract:

"When a future is not ready yet, poll returns Poll::Pending and stores a clone of the Waker copied from the current Context. This Waker is then woken once the future can make progress." Source: std::future::Future

"Once a future has completed (returned Ready from poll), calling its poll method again may panic, block forever, or cause other kinds of problems; the Future trait places no requirements on the effects of such a call." Source: std::future::Future

ALWAYS .await a future exactly once: poll-after-ready is undefined behaviour at the trait level. NEVER call .poll() directly; you almost certainly want .await or a runtime adapter (tokio::pin! + poll_fn).

ALWAYS remember that .await is a suspension point: any value alive across .await becomes part of the generated Future's state and must be Send if the future will run on a multi-threaded executor.


Async blocks

let fut = async {
    let n: u32 = compute().await;
    n + 1
};

An async block is an anonymous future, identical in semantics to the body of an async fn. Capture rules match closures:

  • async { ... } borrows captured variables (like a non-move closure).
  • async move { ... } takes ownership of captured variables.

ALWAYS write async move { ... } when the future will outlive the current scope (for instance, when handing it to tokio::spawn). NEVER assume async { ... } will compile in spawn: spawn requires 'static, which forces async move.


AFIT: async fn in trait (Rust 1.75)

Stabilized in 1.75 (Rust 1.75 release notes):

trait HttpClient {
    async fn get(&self, url: &str) -> Result<String, std::io::Error>;
}

Desugars to:

trait HttpClient {
    fn get(&self, url: &str) -> impl Future<Output = Result<String, std::io::Error>> + use<'_, Self>;
}

Constraints:

  • AFIT methods are not dyn-compatible by themselves. &dyn HttpClient does not work directly because the returned impl Future size is not known at the call site. For dyn you must use Box<dyn Future<Output = T> + Send + '_> explicitly via the trait-variant crate or manual desugaring.
  • The returned future captures &self (or &mut self, or self), so it borrows from the receiver. This makes the future 'a where 'a is the lifetime of the borrow.
  • The Send-ness of the returned future is structural: any !Send value captured (notably across .await inside the body) makes the whole future !Send. There is no syntax in plain AFIT to require the future is Send; for that, use RPITIT explicitly with a Send bound.

ALWAYS use AFIT when the trait is consumed only by static dispatch (generic T: HttpClient). NEVER expect &dyn AsyncTrait to work; use the async-trait crate for true object safety or use the trait-variant crate to generate parallel Send / non-Send versions.

Source: Rust 1.75 release notes


RPITIT: return-position impl Trait in trait (Rust 1.75)

Same stabilization as AFIT:

trait Service {
    fn run(&self) -> impl Future<Output = ()> + Send;
}

Differences from AFIT:

  • You write the return type explicitly, so you can add bounds like + Send, + 'static, or + use<'a>.
  • Each impl may return a different concrete future type, but it must satisfy the declared bounds.
  • Combine with edition 2024 precise-capture (+ use<> opt-out) to drop unwanted lifetime captures.

ALWAYS write -> impl Future<Output = T> + Send (instead of async fn) when callers will tokio::spawn the returned future. NEVER expect AFIT alone to give you Send: that requires RPITIT with an explicit Send bound.

Source: Rust 1.75 release notes


Async closures (Rust 1.85)

Stabilized in 1.85 (Rust 1.85 release notes):

let id: usize = 42;
let task = async move |x: u32| -> u32 {
    do_async_thing(id).await;
    x + 1
};
let v = task(10).await;

Async closures implement the new AsyncFn, AsyncFnMut, AsyncFnOnce traits, which are the async counterparts of Fn, FnMut, FnOnce. Key benefit over Fn() -> impl Future: async closures can borrow from their environment and return a future that borrows from self, which the plain combination of Fn and Future could not express.

fn use_async_fn<F>(_f: F)
where
    F: AsyncFn(u32) -> u32,
{ /* ... */ }

ALWAYS prefer async || when accepting an async callback that needs to borrow from its environment. NEVER reach for Box<dyn Fn(u32) -> Pin<Box<dyn Future<...>>>> first; on edition 2024 + Rust 1.85, AsyncFn is the idiomatic bound.

Source: Rust 1.85 release notes


The Future trait contract

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Source: std::future::Future

Three contract clauses every implementor and every user must respect:

  1. Idempotency-after-Ready is forbidden: once poll has returned Poll::Ready(_), calling poll again on the same future is undefined behaviour at the contract level. (Implementations frequently panic; some loop forever.)
  2. Waker storage on Pending: when poll returns Poll::Pending, the future MUST store a clone of cx.waker() (or assign one already-equivalent) so that the runtime can re-schedule it when progress is possible. On a second poll, the future MUST update to use the most-recent waker if it differs.
  3. Pin<&mut Self> is structural: the receiver is pinned, meaning the future MUST NOT move itself or any field whose pinning is structural (see references/pinning.md).

ALWAYS implement Future only when you genuinely need a leaf future (timer, IO source). NEVER hand-implement Future to combine other futures; use async { ... } or combinators (FuturesUnordered, JoinSet, select!). The state-machine compiler generates correct pinning for you.


Pin / Unpin overview

Pin<P> is a wrapper around a pointer type P (Box<T>, &mut T, Arc<T>, ...) that asserts the pointee MUST NOT be moved in memory until it is dropped. From the standard library:

"From the moment a value is pinned by constructing a Pinning pointer to it, that value must remain, valid, at that same address in memory, until its drop handler is called." Source: std::pin

Unpin is a marker auto-trait. A type T: Unpin is freely movable even when behind Pin. Most types are Unpin. A type that needs to be pinned (self-referential state machines from async fn, intrusive linked-list nodes, ...) opts out with PhantomPinned and is !Unpin.

Symbol What it means Typical use
Pin<&mut T> Pinned mutable borrow Future::poll receiver, Stream::poll_next
Pin<Box<T>> Pinned heap allocation Box::pin(fut), also returned by async fn in some forms
Pin<Arc<T>> Pinned shared-ownership rarely needed by hand; some lock primitives use it
Unpin (auto trait) Safe to move even when pinned every plain struct, every primitive
!Unpin Pinned-by-design state machines from async, types containing PhantomPinned
std::pin::pin!(expr) Stack-pin a value local pinning without heap allocation

For pin projections, structural-vs-non-structural fields, pin_project macro, and manual Future impl boilerplate see references/pinning.md.

ALWAYS take Pin<&mut Self> in poll, never &mut self. NEVER move out of Pin<&mut T> when T: !Unpin. NEVER call Pin::new(&mut some_future) on an !Unpin future built from async; that requires unsafe for !Unpin types. Use pin! or Box::pin instead.


Holding state across .await

Every local that is alive across an .await becomes part of the generated future's state machine. Three consequences:

  1. The state machine is Send only if every such local is Send.
  2. The state machine is Sync only if every such local is Sync.
  3. The state machine's size is bounded by the largest live-set across all suspension points; large locals held across .await blow up future size.

Common !Send traps:

Holding across .await Why !Send Fix
std::sync::MutexGuard Guard is !Send (must be unlocked on the locking thread) Drop guard before .await, or use tokio::sync::Mutex
std::cell::RefMut / Ref RefCell is !Sync; guards are !Send Use tokio::sync::Mutex or restructure to drop before .await
Rc<T> Rc is !Send and !Sync Use Arc<T>
Raw pointer captured from Cell Cell is !Sync Restructure or use atomic
// BAD: future is !Send because MutexGuard is held across .await
async fn bad(mutex: std::sync::Mutex<u32>) {
    let g = mutex.lock().unwrap();   // std::sync guard
    do_io().await;                   // .await here makes future !Send
    println!("{}", *g);
}

// GOOD: scope the guard, drop before .await
async fn good(mutex: std::sync::Mutex<u32>) {
    let v = { let g = mutex.lock().unwrap(); *g };
    do_io().await;
    println!("{}", v);
}

// ALSO GOOD: async mutex from tokio (guard is Send)
async fn good_tokio(mutex: tokio::sync::Mutex<u32>) {
    let g = mutex.lock().await;
    do_io().await;
    println!("{}", *g);
}

ALWAYS scope a std::sync::Mutex lock to a block that does not contain an .await. NEVER hold an Rc across .await if the future will be tokio::spawned.

For more !Send cases and runtime-specific guidance see [[rust-errors-async]] and [[rust-impl-async-tokio]].


Cancellation via drop

Async Rust has no explicit "cancel" method. Cancellation is dropping the future:

  • A future that has not yet completed: its Drop impl (and the drops of every local in its current state) run. After that, the future no longer exists, so no further work happens.
  • A future that is currently suspended at an .await: dropping it cancels at exactly that suspension point. The next .await is never resumed.
  • A future that has already returned Ready: dropping the value is a normal drop.

Implication: anything alive across .await may be dropped at any suspension point. This is structured concurrency by destructor.

ALWAYS write drop-safe code in async bodies: file handles, locks, in-flight network state must all behave correctly when their owning future is dropped mid-.await. NEVER assume code after an .await will run; only code before any given .await is guaranteed to run.

For cancellation safety in select! branches see [[rust-impl-async-tokio]].


Edition 2024: RPIT lifetime capture

Edition 2024 (Rust 1.85 release notes, Edition Guide: RPIT lifetime capture) changes how impl Trait in return position captures generic parameters.

Pre-2024: impl Trait captures only the type parameters explicitly mentioned, not lifetimes.

Edition 2024: impl Trait captures all in-scope generic parameters, including lifetimes. To opt out of capturing a specific lifetime use + use<>:

// Edition 2024: captures 'a implicitly
fn returns_future<'a>(buf: &'a [u8]) -> impl Future<Output = ()> + 'a { async { /* ... */ } }

// Same, written explicitly
fn returns_future<'a>(buf: &'a [u8]) -> impl Future<Output = ()> + use<'a> { async { /* ... */ } }

// Opt-out: do NOT capture 'a (rarely correct for async fn, breaks borrow)
fn returns_future_no_borrow<'a>() -> impl Future<Output = ()> + use<> { async { /* ... */ } }

Why this matters for async:

  • An async fn desugars to an impl Future that must capture the lifetime of &self (and every borrowed argument), or the returned future would dangle.
  • On edition 2024 the default capture is correct. On edition 2021 you sometimes had to write async fn workarounds or Box<dyn Future + '_> because RPIT did not capture lifetimes automatically.

ALWAYS default to the edition-2024 capture rule. NEVER write + use<> (empty capture) on an async function whose body uses any borrowed argument; the returned future would not compile (dangling borrow).

Source: Edition Guide: RPIT lifetime capture Source: Rust 1.82 release notes (precise capturing)


Decision tree: which async return shape?

Need to return a future from a function or trait?

├─ Free function, returns a future?
│   ├─ Use `async fn` if the body is async syntax. Cleanest.
│   └─ Use `fn ... -> impl Future<Output = T> + Send + 'static`
│       when you must add bounds (Send, 'static, use<>) that `async fn` cannot.
│
├─ Trait method?
│   ├─ Generic dispatch only (T: Trait)?
│   │   ├─ AFIT: `async fn m(&self) -> T;`        (1.75)
│   │   └─ RPITIT with bounds: `fn m(&self) -> impl Future<Output = T> + Send;`  (1.75)
│   ├─ Need dyn dispatch (`Box<dyn Trait>`, `&dyn Trait`)?
│   │   ├─ Use `async-trait` crate (boxes the future), OR
│   │   └─ Hand-write `fn m(&self) -> Pin<Box<dyn Future<Output = T> + Send + '_>>`.
│
├─ Higher-order callback?
│   ├─ Rust 1.85 + edition 2024: bound by `AsyncFn(Args) -> Out`. Pass `async ||` closures.
│   └─ Pre-1.85 or compatibility: `F: Fn(Args) -> Fut, Fut: Future<Output = Out>`.
│
└─ Type-erasing a future at runtime (heterogeneous storage)?
    └─ `Pin<Box<dyn Future<Output = T> + Send>>` (alias as `BoxFuture` for ergonomics).

Common compile errors (mini-index)

Error Surface symptom Root cause Fix reference
"future cannot be sent between threads safely" tokio::spawn(my_fut) fails Some !Send value (MutexGuard, Rc, RefCell guard) is alive across .await Scope the value or use async-aware primitive
"cannot move out of Pin<&mut Self>" manual Future::poll body Tried to deref self.foo of a !Unpin field Use Pin::as_mut + projection or pin_project
"cannot use ? operator in an async fn that returns ()" ? in fire-and-forget async Return type is (), no Try impl Return Result<(), E> or use if let Err(_) = res {}
"non-defining use of impl Future" trait with AFIT and dyn AFIT is not dyn-compatible Use async-trait or Pin<Box<dyn Future + Send + '_>>
"implementation of Future is not general enough" HRTB on async fn Future captures 'a, caller wants for<'a> Add + use<'a> or restructure lifetime
"the trait Send is not implemented for Rc<...>" spawn an async block that holds Rc Rc is !Send; use Arc Replace Rc with Arc

For full error explanations and recovery see [[rust-errors-async]].


Reference links

External: