| name | rust-syntax-async-await |
| description | Use when the user writes `async fn`, awaits a future, uses AFIT (1.75) / RPITIT (1.75) / async closures (1.85), holds non-Send state across `.await`, needs the Pin / Unpin semantics, or hits Future-not-Send compile errors. Prevents holding MutexGuard or Rc across `.await`, calling `.poll` directly, confusing async fn with parallel execution, forgetting pinning when implementing Future manually, and edition-2024 RPIT precise-capturing surprises. Covers: `async fn` desugaring, `.await` suspension, async blocks, AFIT (1.75), RPITIT (1.75), async closures (1.85), Future contract (poll idempotency, waker invariant), Pin / Unpin (deep in references/pinning.md), cancellation via drop, holding non-Send across `.await`, edition 2024 RPIT lifetime capture interaction. Keywords: async fn, ".await", "async block", AFIT, RPITIT, "async trait", "async closure", "async ||", Future, "Future not Send", Pin, Unpin, "self-referential", "pinning projection", "structural pinning", "MutexGuard across await", "Send across await", "drop cancel future", "block_on", "cannot move out of pinned", "poll manually", "edition 2024 RPIT".
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Rust 1.85+, edition 2024. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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> {
Ok(String::new())
}
Desugars (conceptually) to:
fn fetch<'a>(url: &'a str) -> impl Future<Output = Result<String, std::io::Error>> + 'a {
async move {
Ok(String::new())
}
}
Three load-bearing facts:
- The return type is anonymous: it is
impl Future<Output = T>, never a nameable type. You cannot write fn returns_future() -> ConcreteFutureName.
- 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.
- 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:
- 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.)
- 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.
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:
- The state machine is
Send only if every such local is Send.
- The state machine is
Sync only if every such local is Sync.
- 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 |
async fn bad(mutex: std::sync::Mutex<u32>) {
let g = mutex.lock().unwrap();
do_io().await;
println!("{}", *g);
}
async fn good(mutex: std::sync::Mutex<u32>) {
let v = { let g = mutex.lock().unwrap(); *g };
do_io().await;
println!("{}", v);
}
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<>:
fn returns_future<'a>(buf: &'a [u8]) -> impl Future<Output = ()> + 'a { async { } }
fn returns_future<'a>(buf: &'a [u8]) -> impl Future<Output = ()> + use<'a> { async { } }
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: