Three places where lifetimes appear without being written: the default lifetime of a trait object, the meaning
of T: 'static, and which lifetimes a returned impl Trait captures (which changed in edition 2024).
Contents
- Default object lifetime rules
- dyn Trait + 'a in structs and returns
- impl dyn Trait blocks
- What T: 'static really means
- E0310 and where 'static comes from
- T: 'a and 'a: 'b bounds
- impl Trait in argument position
- Return-position impl Trait: capture rules
- Precise capturing with use<..> (1.82)
- Migrating to edition 2024
- impl Trait in traits and async fn
Default object lifetime rules
Every trait object type has a lifetime bound: dyn Trait + 'x. When you do not write it, the compiler uses
the default object lifetime bound (this is not elision):
| Written | Means | Rule |
|---|---|---|
Box<dyn Trait> | Box<dyn Trait + 'static> | container type has no lifetime bound on its parameter -> 'static |
Rc<dyn Trait>, Arc<dyn Trait>, Vec<Box<dyn Trait>> | + 'static | same |
&'a dyn Trait | &'a (dyn Trait + 'a) | a reference's lifetime becomes the default |
&'a mut dyn Trait | &'a mut (dyn Trait + 'a) | same |
Ref<'a, dyn Trait> where struct Ref<'a, T: ?Sized + 'a> | dyn Trait + 'a | the container's T: 'a bound is used |
&dyn Trait in a fn signature | &'1 (dyn Trait + '1) | elided reference lifetime, then the rule above |
Box<dyn Trait + '_> in a fn signature | elided lifetime, per normal elision | '_ opts into elision |
dyn Trait where trait Trait: 'a | 'a | the trait's own lifetime bound is the default |
impl dyn Trait { .. } | impl dyn Trait + 'static | see below |
So the most common surprise: a Box<dyn Fn(&str)> field or return type can never hold a closure that
borrows anything non-'static.
dyn Trait + 'a in structs and returns
use std::fmt::Display;
pub struct Report<'a> {
// Each formatter may borrow from the data the report is about.
columns: Vec<Box<dyn Fn(usize) -> String + 'a>>,
}
impl<'a> Report<'a> {
pub fn new() -> Self {
Self { columns: Vec::new() }
}
pub fn column(mut self, f: impl Fn(usize) -> String + 'a) -> Self {
self.columns.push(Box::new(f));
self
}
pub fn row(&self, i: usize) -> Vec<String> {
self.columns.iter().map(|c| c(i)).collect()
}
}
// Returning a boxed iterator that borrows an argument: say '_ (or a named lifetime).
pub fn labels<'a, T: Display>(items: &'a [T]) -> Box<dyn Iterator<Item = String> + 'a> {
Box::new(items.iter().map(|x| x.to_string()))
}
fn main() {
let names = vec!["ada".to_string(), "grace".to_string()];
let report = Report::new().column(|i| names[i].to_uppercase()); // borrows `names`
println!("{:?}", report.row(1));
println!("{:?}", labels(&[1, 2]).collect::<Vec<_>>());
}Without + 'a on the field, column(|i| names[i]...) fails with "names does not live long enough ...
argument requires that names is borrowed for 'static" (or E0521 in a function taking &[String]).
When the object should be 'static (stored in a global registry, sent to a thread), keep the default and own
the captured data (move + String/Arc). static_bounds (LT011) points at Box<dyn ..> fields in
structs that already borrow (struct X<'a>), because that combination is usually a mistake.
For errors, Box<dyn Error + Send + Sync> (implicitly 'static) is the conventional type; it requires error
values that own their data, which is one more reason error structs should not carry lifetimes.
impl dyn Trait blocks
Inherent methods on a trait object type default to dyn Trait + 'static:
pub trait Shape {
fn area(&self) -> f64;
}
// impl dyn Shape { fn describe(&self) -> String { .. } }
// calling describe() on a `&dyn Shape` that borrows non-'static data fails
// (older compilers: E0772 "`self` has an anonymous lifetime `'_` but calling `describe` introduces
// an implicit `'static` lifetime requirement").
impl dyn Shape + '_ {
pub fn describe(&self) -> String {
format!("shape with area {:.1}", self.area())
}
}
struct Square<'a>(&'a f64);
impl Shape for Square<'_> {
fn area(&self) -> f64 {
self.0 * self.0
}
}
fn main() {
let side = 2.0;
let sq = Square(&side);
let s: &dyn Shape = &sq;
println!("{}", s.describe());
}What T: 'static really means
T: 'static reads "values of type T contain no references shorter than 'static" (they own all their
data, or borrow only static data). It says nothing about how long a particular value lives.
| Type | T: 'static? | Why |
|---|---|---|
String, Vec<u8>, u64, Box<str> | yes | owns everything |
Arc<Mutex<Config>> | yes | owns (shares) everything |
&'static str | yes | borrows only static data |
&'a str (non-static 'a) | no | contains a shorter borrow |
Vec<&'a str> | no | contains a shorter borrow |
Cow<'static, str> | yes | |
Box<dyn Fn()> | yes | dyn Fn() + 'static by default |
Common misconceptions:
- "
T: 'staticmeans the value lives until the program ends." AStringsatisfies'staticand is dropped at the end of its scope like any other value. The bound only means it could be kept forever. - "
T: 'staticmeansTis&'static something." Owned types satisfy it too; that is the normal case. - "To fix E0310 I need
Box::leak." You need the value to own its data: convert&strtoString, clone into anArc, or restructure so the borrow does not escape (scoped threads,+ 'aobjects). - "
&'static TandT: 'staticare interchangeable."fn f(x: &'static str)refuses aString's contents;fn f<T: AsRef<str> + 'static>(x: T)accepts aString. - "Adding
'staticeverywhere is harmless." It rejects every borrowed instantiation of a generic and spreads to callers. Ask for it only where data escapes (LT009 flags borrow-only functions).
Box::leak(Box::new(value)) does produce a &'static mut T; reserve it for once-per-process data such as
configuration loaded at startup (or use std::sync::OnceLock, 1.70+, or LazyLock, 1.80+).
E0310 and where 'static comes from
The requirement always has a source. Find it in the diagnostic ("...so that the type T will meet its
required lifetime bounds", "argument requires that ... is borrowed for 'static") and decide at that point.
Source of 'static | Why it needs it | Options |
|---|---|---|
std::thread::spawn | thread may outlive the caller | move owned data / Arc; std::thread::scope (1.63+) to borrow |
tokio::spawn | task may outlive the caller | owned data / Arc; JoinSet still needs 'static; restructure as join! / FuturesUnordered in the same task to borrow |
Box<dyn Trait> (default) | object bound defaults to 'static | Box<dyn Trait + 'a> |
dyn Any, TypeId::of::<T>(), downcast_ref | Any: 'static | own the data; Any cannot hold borrows |
static / OnceLock / lazy_static | lives for the whole program | own the data |
a trait with trait Plugin: 'static | supertrait bound | remove the bound if not needed |
use std::any::Any;
use std::fmt::Debug;
// E0310 without 'static: Box<dyn Any> requires T: 'static.
fn erase<T: Any>(value: T) -> Box<dyn Any> {
Box::new(value)
}
// Only borrowing T: no 'static needed.
fn show<T: Debug + ?Sized>(value: &T) -> String {
format!("{value:?}")
}
fn main() {
let any = erase(String::from("owned"));
assert!(any.downcast_ref::<String>().is_some());
let local = String::from("borrowed");
let words: Vec<&str> = local.split(' ').collect();
println!("{}", show(&words)); // Vec<&str> is not 'static: fine here
}T: 'a and 'a: 'b bounds
T: 'a- every reference insideToutlives'a. Required to form&'a T. Since Rust 1.31 (RFC 2093) these bounds are inferred for struct fields, sostruct Ref<'a, T> { r: &'a T }needs noT: 'a. You still write them on trait objects (Box<dyn Trait + 'a>), on generic functions that build&'a Tfrom scratch, and on GAT where clauses (where Self: 'a).'long: 'short-'longoutlives'short. Needed when a function returns data of one lifetime through a reference of another, or a struct holds two related borrows.
// The cache reference may be shorter than the data it hands out.
struct Cache<'data> {
items: Vec<&'data str>,
}
fn first<'c, 'data: 'c>(cache: &'c Cache<'data>) -> &'data str {
cache.items.first().copied().unwrap_or("")
}('data: 'c is implied here by the type &'c Cache<'data> being well-formed, so it can be omitted; writing
it documents the relationship.)
impl Trait in argument position
fn f(x: impl Trait) is sugar for an anonymous generic parameter. Lifetimes inside it follow the normal rules:
impl Fn(&str) -> &str is higher-ranked over the reference, and an impl Iterator<Item = &str> argument gets
a fresh elided lifetime. The caller cannot name the type with turbofish.
Return-position impl Trait: capture rules
The returned hidden type (say Filter<Iter<'a, u32>, {closure}>) may only mention generic parameters the
opaque type captures. Capturing a lifetime means callers must treat the result as borrowing it.
| Position / edition | Type & const params | Lifetime params (incl. elided) |
|---|---|---|
| free fn / inherent method, edition 2015-2021 | all in scope captured | only those named in the bounds (+ 'a, + '_, + use<'a>) |
| free fn / inherent method, edition 2024 | all captured | all in scope captured |
async fn (all editions) | all | all |
-> impl Trait in a trait or trait impl (RPITIT, 1.75+) | all | all |
with + use<..> (1.82+) | exactly the listed ones (in 1.82 every type param must be listed) | exactly the listed ones |
Edition 2021 failure (E0700) and its fixes:
// error[E0700] in edition 2021: hidden type captures lifetime that does not appear in bounds
// fn evens(xs: &[u32]) -> impl Iterator<Item = u32> { xs.iter().copied().filter(|x| x % 2 == 0) }
fn evens(xs: &[u32]) -> impl Iterator<Item = u32> + '_ {
xs.iter().copied().filter(|x| x % 2 == 0)
}+ 'a is an outlives bound that happens to force capture. With two lifetimes it is wrong:
fn f<'a, 'b>(x: &'a str, y: &'b str) -> impl Iterator<Item = char> + 'a cannot return something that
captures 'b. Before use<..> the workaround was the Captures trick:
trait Captures<'a> {}
impl<'a, T: ?Sized> Captures<'a> for T {}
// Pre-1.82, edition 2021: capture both lifetimes without claiming the result outlives either.
fn zip_chars<'a, 'b>(x: &'a str, y: &'b str) -> impl Iterator<Item = (char, char)> + Captures<'a> + Captures<'b> {
x.chars().zip(y.chars())
}With Rust 1.82+ write -> impl Iterator<Item = (char, char)> + use<'a, 'b> instead, or rely on edition 2024.
Precise capturing with use<..> (1.82)
use<..> lists exactly what the opaque type may capture. It works in every edition from Rust 1.82.
// Edition 2024: without use<T>, this would capture the slice lifetime and keep `v` borrowed.
fn indices<T>(xs: &[T]) -> impl Iterator<Item = usize> + use<T> {
0..xs.len()
}
fn main() {
let mut v = vec![10, 20, 30];
let it = indices(&v);
v.push(40); // allowed: `it` does not borrow `v`
for i in it {
println!("{i}");
}
}Rules worth knowing:
use<>captures nothing;use<'a, T>captures'aandT.- In Rust 1.82 all type and const parameters in scope must appear in the list; only lifetimes can be omitted.
- Only one
use<..>bound is allowed perimpl Trait. - In trait definitions (RPITIT) all parameters are always captured;
use<..>support there arrived after 1.82, so check your toolchain before using it in traits.
Migrating to edition 2024
cargo fix --editionruns theimpl_trait_overcaptureslint, which addsuse<..>where the new rules would capture more than before and change borrow-checking for callers.- After migrating, new code can usually drop
+ '_/Capturesworkarounds: the default now captures. - Add
use<..>when callers need to keep using an argument while holding the result (iterators over owned copies, builders that return an independent value). - Edition 2024 needs Rust 1.85+ and
edition = "2024"inCargo.toml.lifetime_smells(LT012) lists every-> impl Traitwith borrowed inputs and nouse<..>when the crate is on 2024.
impl Trait in traits and async fn
async fnin traits and-> impl Traitin traits are stable since Rust 1.75. Their returned types capture all generic parameters, including all input lifetimes.- Callers cannot add
Sendto the returned future of a trait'sasync fn. If implementors are used withtokio::spawn, declare the method asfn get(&self) -> impl Future<Output = T> + Send;(implementors can still writeasync fn) or use thetrait-variantcrate to generate aSendvariant. async fnin traits is not dyn-compatible (object safe); fordyn Traituse `Pin<Box<dyn Future + Send- '_>>
returns (theasync-traitcrate generates exactly that, with'async_trait` lifetimes).
- '_>>
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-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-trait-design-reviewer
Review and design Rust traits and generic APIs: generics vs impl Trait vs dyn Trait (static vs dynamic dispatch), dyn compatibility / object safety (generic methods, Self returns, where Self: Sized, error[E0038] not dyn compatible / cannot be made into an object), associated types vs generic parameters, blanket impls, coherence and orphan rule (E0117, E0119, E0210), newtype, extension, sealed and marker traits, supertraits and trait upcasting (1.86), async fn in traits (1.75) and the Send bound problem (trait_variant, async-trait), RPITIT, GATs, From/Into/TryFrom/AsRef/Borrow, std trait checklist (Debug, Clone, PartialEq, Hash, Default, Display, Error, Send/Sync, serde feature), #[must_use], #[non_exhaustive], semver hazards, Deref abuse, builders, typestate, PhantomData, Box<dyn Error + Send + Sync>, Fn traits, mockall.
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.