async fn and -> impl Trait in traits (Rust 1.75), the Send bound problem and its workarounds, boxing
for dyn, generic associated types (1.65), associated types vs generic parameters, associated type bounds
(1.79), higher-ranked trait bounds, and the Fn/AsyncFn closure traits.
Contents
- async fn in traits
- The Send bound problem
- Fix 1: declare the Send future in the trait
- Fix 2: trait_variant
- Fix 3: dyn with async methods
- Return-type notation (not stable)
- Decision table for async traits
- RPITIT: -> impl Trait in traits
- Associated types vs generic parameters
- Associated type bounds and associated consts
- Generic associated types
- Higher-ranked trait bounds
- Fn, FnMut, FnOnce and function pointers
- Async closures and AsyncFn (1.85)
- Review checklist
async fn in traits
Since Rust 1.75 a trait may declare async fn directly:
pub trait Repo {
async fn load(&self, id: u64) -> Option<String>;
}
pub struct MemRepo(Vec<String>);
impl Repo for MemRepo {
async fn load(&self, id: u64) -> Option<String> {
self.0.get(id as usize).cloned()
}
}
// Generic use works out of the box.
pub async fn load_two<R: Repo>(repo: &R) -> (Option<String>, Option<String>) {
(repo.load(0).await, repo.load(1).await)
}It desugars to fn load(&self, id: u64) -> impl Future<Output = Option<String>>, an anonymous associated type
per implementation. Two consequences:
- The trait is not dyn compatible (an opaque return type has no single vtable signature).
- Callers cannot name that future, so they cannot add bounds to it, most importantly
Send.
Because of (2), rustc warns on async fn in a publicly reachable trait:
warning: use of `async fn` in public traits is discouraged as auto trait bounds cannot be specified
= note: you can suppress this lint if you plan to use the trait only in your own code, or do not care
about auto traits like `Send` on the `Future`
= note: `#[warn(async_fn_in_trait)]` on by defaultThe Send bound problem
Multi-threaded executors (tokio::spawn, std::thread::spawn-backed pools) need Send futures. With
async fn in the trait, generic code cannot prove the future is Send, even when every concrete impl is:
pub async fn spawn_load<R: Repo + Send + Sync + 'static>(repo: std::sync::Arc<R>) {
tokio::spawn(async move {
repo.load(1).await // error: future cannot be sent between threads safely
});
}R: Send + Sync is not enough: the future returned by R::load is a separate type whose Send-ness is
unknown in generic code. Concrete types do not have the problem (the compiler sees the actual future), which is
why the error often appears only after a function is made generic.
Fix 1: declare the Send future in the trait
Write the desugared form with the bounds you need. Implementors can still use async fn in their impls, as
long as their future actually is Send:
use std::future::Future;
pub trait Repo: Send + Sync {
fn load(&self, id: u64) -> impl Future<Output = Option<String>> + Send;
}
pub struct MemRepo(Vec<String>);
impl Repo for MemRepo {
async fn load(&self, id: u64) -> Option<String> {
self.0.get(id as usize).cloned()
}
}
fn require_send<T: Send>(value: T) -> T {
value // stands in for tokio::spawn, which requires Send
}
// Compiles in generic code: the trait guarantees the future is Send.
pub async fn load_first<R: Repo>(repo: &R) -> Option<String> {
require_send(repo.load(0)).await
}Cost: implementors that hold Rc, RefCell borrows or MutexGuards across .await can no longer implement
the trait. That is usually what a library wants.
Fix 2: trait_variant
The trait-variant crate (from the Rust async working group) generates the Send variant from a local one:
// Cargo.toml: trait-variant = "0.1"
#[trait_variant::make(Repo: Send)]
pub trait LocalRepo {
async fn load(&self, id: u64) -> Option<String>;
}This keeps LocalRepo (no Send requirement, for single-threaded runtimes) and generates Repo, whose
methods return impl Future<Output = ..> + Send, plus a blanket impl so every Repo is also a LocalRepo.
Implement Repo for thread-safe types and write generic multi-threaded code against Repo.
Fix 3: dyn with async methods
Native async traits are not dyn compatible. Options:
async-trait crate (boxes every returned future):
// Cargo.toml: async-trait = "0.1"
use async_trait::async_trait;
#[async_trait]
pub trait Store: Send + Sync {
async fn get(&self, key: &str) -> Option<String>;
}
pub struct Mem(std::collections::HashMap<String, String>);
#[async_trait]
impl Store for Mem {
async fn get(&self, key: &str) -> Option<String> {
self.0.get(key).cloned()
}
}
// Works: Box<dyn Store>, Arc<dyn Store>, &dyn Store.
pub async fn read(store: &dyn Store) -> Option<String> {
store.get("k").await
}The macro rewrites methods to return Pin<Box<dyn Future<Output = T> + Send + 'async_trait>>. Futures are
Send by default; #[async_trait(?Send)] on both the trait and impls drops that. Cost: one heap allocation
per call, which is negligible next to network or disk I/O.
Hand-written boxing (no macro, same shape):
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
pub trait Store: Send + Sync {
fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Option<String>>;
}
pub struct Mem(HashMap<String, String>);
impl Store for Mem {
fn get<'a>(&'a self, key: &'a str) -> BoxFuture<'a, Option<String>> {
Box::pin(async move { self.0.get(key).cloned() })
}
}
pub async fn read(store: &dyn Store) -> Option<String> {
store.get("k").await
}Lifetime parameters on methods are allowed in dyn-compatible traits; type parameters are not. futures::future::BoxFuture
is the same alias if you already depend on futures.
Both static and dyn: keep the native trait for generic callers and add an object-safe twin with a blanket impl:
use std::future::Future;
use std::pin::Pin;
pub trait Fetch: Send + Sync {
fn fetch(&self, url: &str) -> impl Future<Output = String> + Send;
}
pub trait DynFetch: Send + Sync {
fn fetch_dyn<'a>(&'a self, url: &'a str) -> Pin<Box<dyn Future<Output = String> + Send + 'a>>;
}
impl<T: Fetch> DynFetch for T {
fn fetch_dyn<'a>(&'a self, url: &'a str) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
Box::pin(self.fetch(url))
}
}Return-type notation (not stable)
The language-level fix is return-type notation, letting callers bound a method's future:
fn spawn_load<R: Repo<load(..): Send>>(..). As of this writing it is available only on nightly
(#![feature(return_type_notation)]). Do not design a stable public API around it; use Fix 1 or Fix 2.
Decision table for async traits
| Situation | Choose |
|---|---|
| Trait used only inside your crate, concrete types or single-threaded runtime | async fn, #[allow(async_fn_in_trait)] if it is pub but internal |
| Public trait, generic callers on Tokio multi-thread | -> impl Future<Output = T> + Send or #[trait_variant::make(X: Send)] |
Need Box<dyn Trait> / Arc<dyn Trait> (plugins, DI containers) | #[async_trait] or hand-written BoxFuture |
| Hot path, millions of calls per second, plus dyn | hand-written boxing per call is measurable: consider enum dispatch or a generic API |
| MSRV below 1.75 | async-trait or BoxFuture |
RPITIT: -> impl Trait in traits
Also stable since 1.75: methods may return impl Trait:
pub trait Catalog {
fn names(&self) -> impl Iterator<Item = &str>;
}
pub struct Shelf(Vec<String>);
impl Catalog for Shelf {
fn names(&self) -> impl Iterator<Item = &str> {
self.0.iter().map(String::as_str)
}
}
pub fn count<C: Catalog>(c: &C) -> usize {
c.names().count()
}- Each impl picks its own concrete type; no
Box, no allocation. - Not dyn compatible: offer
fn names_boxed(&self) -> Box<dyn Iterator<Item = &str> + '_>ifdynis needed. - RPITIT captures all generic parameters and lifetimes in scope, including the
&selflifetime. - Auto traits are not implied: add
+ Sendin the trait if generic callers need it, exactly like futures. - An impl may return a more specific type than the trait promises (refinement), but for public traits rustc
warns (
refining_impl_trait) because callers through the trait cannot rely on it.
Associated types vs generic parameters
use std::ops::Add;
// Generic parameter (with default): a type can implement Add many times, once per Rhs.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Money {
cents: i64,
}
impl Add for Money {
type Output = Money; // associated type: one Output per (Self, Rhs) pair
fn add(self, rhs: Money) -> Money {
Money { cents: self.cents + rhs.cents }
}
}
impl Add<i64> for Money {
type Output = Money;
fn add(self, rhs: i64) -> Money {
Money { cents: self.cents + rhs }
}
}
// Associated type: a parser produces exactly one kind of output.
pub trait Parser {
type Output;
type Error;
fn parse(&self, input: &str) -> Result<Self::Output, Self::Error>;
}
fn main() {
let a = Money { cents: 150 };
assert_eq!(a + a, Money { cents: 300 });
assert_eq!(a + 50_i64, Money { cents: 200 });
}| Pick | When | Examples |
|---|---|---|
| Associated type | the type is determined by the implementor; one impl per type | Iterator::Item, Deref::Target, FromStr::Err, Future::Output, Service::Response |
| Generic parameter | the same type implements the trait for several argument types | From<T>, AsRef<T>, Add<Rhs>, PartialEq<Rhs>, Service<Request> |
| Both | input chosen by caller, output determined by the pair | Add<Rhs> { type Output; }, tower Service<Request> { type Response; } |
Symptoms of the wrong choice:
- Generic parameter where an associated type belongs: callers need annotations everywhere,
error[E0283] "type annotations needed", and bounds like
T: Parser<u32, MyErr>repeated. - Associated type where a parameter belongs: users cannot implement the trait twice for the same type and reach for wrapper types.
Trait-level generic parameters can have defaults (trait Add<Rhs = Self>); adding a defaulted parameter to an
existing trait is a minor change, adding one without a default is major.
Associated type bounds and associated consts
Associated type bounds (Rust 1.79) put a bound on an associated type inline:
use std::fmt::Display;
pub fn join<I>(items: I) -> String
where
I: IntoIterator<Item: Display>,
{
items.into_iter().map(|i| i.to_string()).collect::<Vec<_>>().join(",")
}
// Before 1.79: where I: IntoIterator, I::Item: Display
fn main() {
assert_eq!(join([1, 2]), "1,2");
}Associated consts (Rust 1.20) give per-type constants usable in generic code, but make the trait not dyn compatible:
pub trait Codec {
const NAME: &'static str;
const MAX_FRAME: usize = 64 * 1024; // default
fn encode(&self, out: &mut Vec<u8>);
}
pub fn header<C: Codec>() -> String {
format!("{} (max {} bytes)", C::NAME, C::MAX_FRAME)
}Const generics (Rust 1.51) let traits be implemented over array lengths: impl<const N: usize> Codec for [u8; N].
Generic associated types
GATs (Rust 1.65) let an associated type take generic parameters, most often a lifetime tied to &self.
The canonical use is a "lending" iterator whose items borrow from the iterator itself, which Iterator cannot
express:
pub trait LendingIterator {
type Item<'a>
where
Self: 'a;
fn next(&mut self) -> Option<Self::Item<'_>>;
}
pub struct WindowsMut<'s, T> {
slice: &'s mut [T],
start: usize,
size: usize,
}
impl<'s, T> LendingIterator for WindowsMut<'s, T> {
type Item<'a> = &'a mut [T] where Self: 'a;
fn next(&mut self) -> Option<Self::Item<'_>> {
let window = self.slice.get_mut(self.start..self.start + self.size)?;
self.start += 1;
Some(window)
}
}
fn main() {
let mut data = [1, 2, 3, 4];
let mut it = WindowsMut { slice: &mut data, start: 0, size: 2 };
while let Some(w) = it.next() {
w[1] += w[0];
}
assert_eq!(data, [1, 3, 6, 10]);
}- The
where Self: 'aclause is required by the compiler for GATs that borrow fromSelf. - GATs make a trait not dyn compatible.
- Generic code over GATs can hit borrow-checker limitations with higher-ranked bounds
(
for<'a> I::Item<'a>: Debug); keep GAT traits small and test the generic consumers you expect. - Prefer a plain associated type or
Iteratorwhen items do not borrow from the producer.
Higher-ranked trait bounds
for<'a> says "for every lifetime": needed when a closure or trait must work with borrows created inside your
function.
// Elided form: Fn(&str) -> &str already means for<'a> Fn(&'a str) -> &'a str.
pub fn apply_to_lines<F>(text: &str, f: F) -> Vec<&str>
where
F: Fn(&str) -> &str,
{
text.lines().map(|l| f(l)).collect()
}
// Explicit form needed for non-Fn traits:
pub trait Visit<'a> {
fn visit(&mut self, item: &'a str);
}
pub fn visit_all<V>(lines: &[String], v: &mut V)
where
V: for<'a> Visit<'a>,
{
for l in lines {
v.visit(l);
}
}
fn main() {
let out = apply_to_lines(" a \n b", str::trim);
assert_eq!(out, ["a", "b"]);
}Symptom of a missing HRTB: "borrowed value does not live long enough" / "implementation of X is not general enough" when a generic bound fixes one lifetime that the function body needs to vary.
Fn, FnMut, FnOnce and function pointers
| Trait | Callable | Captures may be | Accept it when |
|---|---|---|---|
FnOnce() | once (consumes) | moved out | you call it at most once (Option::map, thread::spawn) |
FnMut() | many times, &mut | mutated | you call it repeatedly, sequentially (Iterator::for_each) |
Fn() | many times, & | only read | you call it concurrently or through & (shared handlers) |
fn() pointer | many times | no captures | C callbacks, tables of functions, Copy + 'static needed |
Every Fn is FnMut, every FnMut is FnOnce. Ask for the least capable trait that works: FnOnce accepts
the most closures.
use std::collections::HashMap;
// Parameters: generic (static dispatch) is the default.
pub fn retry<T, E>(attempts: u32, mut op: impl FnMut() -> Result<T, E>) -> Result<T, E> {
let mut left = attempts.max(1);
loop {
match op() {
Ok(v) => return Ok(v),
Err(e) if left == 1 => return Err(e),
Err(_) => left -= 1,
}
}
}
// Stored callbacks: boxed trait objects, Send + Sync if shared across threads.
type Handler = Box<dyn Fn(&str) -> String + Send + Sync>;
#[derive(Default)]
pub struct Router {
routes: HashMap<String, Handler>,
}
impl Router {
pub fn on(&mut self, path: &str, h: impl Fn(&str) -> String + Send + Sync + 'static) {
self.routes.insert(path.to_owned(), Box::new(h));
}
pub fn call(&self, path: &str, body: &str) -> Option<String> {
self.routes.get(path).map(|h| h(body))
}
}
// Returning closures: impl Fn (one concrete type) or Box<dyn Fn> (several).
pub fn adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
fn main() {
let mut calls = 0;
let r: Result<u8, &str> = retry(3, || {
calls += 1;
if calls < 3 { Err("flaky") } else { Ok(7) }
});
assert_eq!(r, Ok(7));
let mut router = Router::default();
router.on("/echo", |b| b.to_uppercase());
assert_eq!(router.call("/echo", "hi").as_deref(), Some("HI"));
assert_eq!(adder(2)(3), 5);
}Router has no Debug derive because dyn Fn is not Debug; implement Debug manually listing the route
keys if it is public.
Async closures and AsyncFn (1.85)
Rust 1.85 stabilized async closures (async || { .. }, async move |x| ..) and the AsyncFn,
AsyncFnMut, AsyncFnOnce traits. They let a callback return a future that borrows from the closure's
captures, which F: Fn() -> Fut cannot express.
pub async fn with_retry<T, E>(attempts: u32, op: impl AsyncFn() -> Result<T, E>) -> Result<T, E> {
let mut left = attempts.max(1);
loop {
match op().await {
Ok(v) => return Ok(v),
Err(e) if left == 1 => return Err(e),
Err(_) => left -= 1,
}
}
}There is no stable way to require the future returned by an AsyncFn to be Send. For callbacks whose
futures are spawned on a multi-threaded runtime keep the older form:
use std::future::Future;
pub async fn with_retry_send<T, E, F, Fut>(attempts: u32, op: F) -> Result<T, E>
where
F: Fn() -> Fut,
Fut: Future<Output = Result<T, E>> + Send,
{
let mut left = attempts.max(1);
loop {
match op().await {
Ok(v) => return Ok(v),
Err(e) if left == 1 => return Err(e),
Err(_) => left -= 1,
}
}
}Review checklist
- Public traits with async methods state their Send story (impl Future + Send, trait_variant, async_trait).
- Traits used as
dynhave noasync fn/RPITIT methods (or opt them out withwhere Self: Sized). -
#[allow(async_fn_in_trait)]only on traits that are public for internal reasons. - Associated types for outputs determined by
Self; generic parameters for inputs chosen by callers. - Callback parameters ask for
FnOnce<FnMut<Fn(least capable that works); stored callbacks areBox<dyn Fn.. + Send + Sync>when shared. - GAT traits are small, tested with the generic consumers you intend, and not needed for
dyn. - MSRV noted for AFIT/RPITIT (1.75), associated type bounds (1.79), async closures (1.85).
rust-borrow-checker-error-explainer
Explain and fix Rust borrow-checker, ownership, move and lifetime compiler errors: E0382 use of moved value, E0499 cannot borrow as mutable more than once, E0502 also borrowed as immutable, E0505, E0506, E0503, E0507 cannot move out of index/Rc/reference, E0515 return reference to local, E0597 does not live long enough, E0716 temporary value dropped while borrowed, E0373 closure may outlive (spawn needs move), E0521 borrowed data escapes, E0384, E0596, E0594, E0381, E0713, 'lifetime may not live long enough', RefCell 'already borrowed: BorrowMutError' panics. Ranks fixes from restructuring (split borrows, entry API, split_at_mut, get_disjoint_mut, mem::take, Option::take, indices, thread::scope) down to clone and Rc<RefCell>/Arc<Mutex>; never unsafe. Covers NLL limits (get-or-insert), iterator invalidation, closure captures, self-referential structs. Ships std-only Rust triage tools.
rust-cargo-workspace-designer
Design, review and fix Cargo workspaces: when to split a crate (compile time, boundaries, publishing, proc-macros), crates/* vs apps/ + crates/ layouts, virtual manifest vs root package, members/exclude/default-members, resolver 2 vs 3 (MSRV-aware, edition 2024), [workspace.package] and [workspace.dependencies] inheritance, [workspace.lints], root-only profiles (lto, codegen-units), additive features with dep: and weak ?/ syntax, feature unification pitfalls, internal path + version deps, publish = false, Cargo.lock and MSRV policy, xtask, CI with --locked, nextest, rust-cache, sccache, cargo-hack/deny/machete/udeps/semver-checks, and cargo build --timings. Ships a workspace auditor and crate-graph tool.
rust-error-handling-thiserror-anyhow-designer
Design and review Rust error handling with thiserror 2 and anyhow 1: library vs application split, typed error enums with #[from], #[source], #[error(transparent)] and #[non_exhaustive], ErrorKind patterns, context via .context()/.with_context(), bail!/ensure!, source() chains, {:#} and {:?} reports, RUST_BACKTRACE/RUST_LIB_BACKTRACE, Box<dyn Error + Send + Sync>, main -> anyhow::Result or ExitCode, unwrap/expect/panic policy, Option to Result, collecting iterators of Results, axum IntoResponse mapping, retryable errors, tracing, async Send bounds, testing with matches! and downcast_ref, no_std core::error::Error, and snafu, eyre/color-eyre, miette, error-stack.
rust-lifetime-annotation-advisor
Decide when Rust lifetime annotations are needed and which ones: the three elision rules, structs holding references (struct Parser<'a>), owned vs borrowed (String, &str, Cow<'a, str>, Arc<str>), one vs several lifetimes, 'a: 'b bounds, T: 'static misconceptions, Box<dyn Trait + 'a> default object lifetimes, impl Trait + '_ and edition 2024 capture rules with use<..>, variance and PhantomData, HRTB for<'a> Fn(&'a T), GATs and lending iterators, async fn and tokio::spawn 'static, self-referential structs (ouroboros, self_cell, indices, arenas), Rc/Arc to escape lifetime soup. Ships std-only Rust checkers for redundant, missing, unused and over-restrictive lifetimes.
rust-tokio-async-service-builder
Build and review production async Rust services on Tokio 1.x and axum 0.8: runtime choice (#[tokio::main] multi_thread vs current_thread, Builder), axum routing with /{id} paths, State, extractors, IntoResponse error types, axum::serve with_graceful_shutdown, tower/tower-http middleware (TraceLayer, TimeoutLayer, CorsLayer, ConcurrencyLimitLayer, RequestBodyLimitLayer), Arc<AppState>, std vs tokio Mutex, JoinSet, TaskTracker, CancellationToken, select! cancel safety, bounded mpsc, oneshot, broadcast, watch, actors, Semaphore, timeouts, retries, spawn_blocking, sqlx PgPool, reqwest Client reuse, tracing, tests with start_paused and oneshot, Dockerfile. Fixes 'future cannot be sent between threads safely', E0373, Rc or MutexGuard held across await, 'Cannot start a runtime from within a runtime', 'Path segments must not start with :'. Ships std-only Rust scanners and a compiling axum 0.8 scaffold.
rust-unsafe-block-auditor
Audit unsafe Rust for soundness: inventory every unsafe block, unsafe fn, unsafe impl/trait, extern block and unsafe attribute, check // SAFETY: comments and # Safety docs, and classify each as sound, needs-justification or likely UB, proposing safe alternatives first. Covers the five unsafe superpowers, aliasing (Stacked/Tree Borrows), invalid values, MaybeUninit vs mem::zeroed/uninitialized, transmute, slice::from_raw_parts, Vec::set_len/from_raw_parts, get_unchecked, CString::as_ptr dangling, unsafe impl Send/Sync, Pin projection, FFI (extern "C", repr(C), C-unwind, Box::into_raw/from_raw), strict provenance, static mut and &raw, edition 2024 (unsafe extern, #[unsafe(no_mangle)], unsafe_op_in_unsafe_fn, env::set_var), Miri, sanitizers, cargo-fuzz, loom, cargo-geiger and clippy safety lints.