| name | rust-lifetimes |
| description | > Use when this capability is needed. |
Lifetimes
Lifetimes are a variety of generics that tell the borrow checker how long references must
remain valid. Every reference already has a lifetime; annotations only make implicit
relationships explicit so the compiler can verify them. Lifetimes prevent dangling
references at zero runtime cost — they are erased before codegen.
Book: "Validating References with Lifetimes" — Chapter 10 §3
(https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html)
When to Use
Do annotate when:
- A function takes multiple reference parameters and returns a reference — the compiler
cannot infer which input the output borrows from.
- A struct holds a reference field — the struct must not outlive the data it borrows.
- A
where clause is needed to satisfy T: 'a (E0309) or T: 'static (E0310) — note
that when a struct field is &'a T, rustc edition 2021+ implies T: 'a automatically;
the explicit bound is only required when the struct does not directly hold &'a T.
Do NOT annotate when the three elision rules already cover the case (see Core Idioms).
Redundant annotations add noise and are a Forbidden pattern.
Core Idioms
Elision covers it — do not annotate
fn first_word(s: &str) -> &str { ... }
fn first_word<'a>(s: &'a str) -> &'a str { ... }
Multiple inputs returning a reference — annotate
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn longest(x: &str, y: &str) -> &str { ... }
Struct holding a reference — lifetime required
struct Excerpt<'a> {
part: &'a str,
}
struct Excerpt {
part: &str,
}
Method with &self — elision rule 3 applies
impl<'a> Excerpt<'a> {
fn level(&self) -> i32 { 3 }
fn announce(&self, note: &str) -> &str { self.part }
}
fn announce<'b>(&'b self, note: &str) -> &'b str { self.part }
Lifetime bound on a generic type parameter
struct Wrapper<'a, T> {
value: &'a T,
}
struct Forever<T> {
value: &'static T,
}
Note (Rust edition 2021+): When a struct field is &'a T, the compiler implies the
T: 'a bound automatically. Writing T: 'a explicitly is accepted but not required.
The explicit form is genuinely needed only when the struct does not directly hold a
&'a T field but still needs the invariant — for example, via an associated type
(T::Output: 'a) or a complex generic constraint where inference cannot fill it in.
The Three Lifetime Elision Rules
(Book: "Lifetime Elision" subsection of ch10-03)
The compiler applies these rules in order before requiring explicit annotations.
-
Each reference parameter gets its own lifetime.
fn foo(x: &i32, y: &i32) → fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
-
If exactly one input lifetime, it flows to all output lifetimes.
fn foo<'a>(x: &'a str) -> &str → output gets 'a automatically.
-
If one of the inputs is &self or &mut self, its lifetime flows to all outputs.
Methods rarely need explicit return-lifetime annotations.
If the rules exhaust without resolving every output lifetime the compiler emits E0106.
Forbidden Patterns
Forbidden 1 — Slapping 'static to Silence E0597
Forbidden: Adding a 'static bound or changing a return type to &'static T solely
to stop "does not live long enough" errors.
Why: 'static means the data lives for the entire program. Forcing it hides the real
problem — a reference outliving its owner — rather than fixing the ownership relationship.
Book: "The Static Lifetime" — "most of the time, an error message suggesting the
'static lifetime results from attempting to create a dangling reference or a mismatch of
the available lifetimes."
fn get_name<'a>(s: &'a str) -> &'static str {
s
}
fn get_name<'a>(s: &'a str) -> &'a str {
s
}
grep -rn "'static" src/ | grep -v '"' | grep -v '// @intentional-static'
Forbidden 2 — Returning a Reference to a Local Variable (E0515)
Forbidden: Returning a &T that points to a value created inside the function.
Why: The local is dropped at the closing brace; the reference would dangle. The borrow
checker enforces this as E0515 ("cannot return value referencing local variable"). Book:
"Generic Lifetimes in Functions" — "If the reference returned does not refer to one of
the parameters, it must refer to a value created within this function. However, this would
be a dangling reference because the value will go out of scope at the end of the function."
fn make_str<'a>() -> &'a str {
let result = String::from("hello");
result.as_str()
}
fn make_str() -> String {
String::from("hello")
}
grep -rn 'let.*=.*String::from\|let.*=.*Vec::new\|let.*=.*format!' src/ \
| grep -v '#\[cfg(test'
Forbidden 3 — Redundant Explicit Lifetimes Elision Already Covers
Forbidden: Annotating a function with <'a> and &'a parameters when all three
elision rules already resolve to the same result.
Why: Redundant annotations increase cognitive load and signal that the author did not
apply the elision rules. Book: "Lifetime Elision" — "After writing a lot of Rust code,
the Rust team found that Rust programmers were entering the same lifetime annotations over
and over."
fn first_word<'a>(s: &'a str) -> &'a str {
let boundary = s.find(' ').unwrap_or(s.len());
&s[..boundary]
}
fn first_word(s: &str) -> &str {
let boundary = s.find(' ').unwrap_or(s.len());
&s[..boundary]
}
grep -rnE "fn .*<.*'[a-z][a-z]*.*>.*&.*'[a-z]" src/ --include='*.rs' \
| grep -v struct | grep -v impl
Forbidden 4 — Box<dyn Trait + 'static> Where a Scoped Borrow Suffices
Forbidden: Using Box<dyn Trait + 'static> (or Arc<dyn Trait + 'static>) for
trait objects that are only needed for the duration of a known scope.
Why: 'static rules out any borrowed data from being placed in the box, forcing
unnecessary cloning or Arc-wrapping of values that have a natural shorter lifetime. The
correct bound is the scope's lifetime: Box<dyn Trait + 'a>. Book: "The Static
Lifetime" — "the solution is to fix those problems, not to specify the 'static lifetime."
fn process(handler: Box<dyn Fn() + 'static>) { ... }
fn process<'a>(handler: Box<dyn Fn() + 'a>) { ... }
fn process(handler: impl Fn()) { ... }
grep -rn "dyn.*'static\|'static.*dyn" src/ | grep -v '// @static-required'
Forbidden 5 — Self-Referential Structs (Without Pin/Owning)
Forbidden: A struct that holds a reference to data owned by another field of the same
struct (e.g., struct S { data: String, view: &'??? str }).
Why: When the struct moves, data moves to a new address and the reference dangles.
Rust's ownership model forbids stable addresses for ordinary stack/heap values. The
correct solutions are: (a) use an owned type (String) instead of a borrow, (b) use an
index into the data, (c) use a crate like ouroboros or self_cell that wraps the
struct in Pin, or (d) separate the data and the view into two distinct structs. Book:
"In Struct Definitions" — "An instance of ImportantExcerpt can't outlive the reference
it holds in its part field."
struct Parser {
input: String,
current: &str,
}
struct Parser {
input: String,
cursor: usize,
}
grep -rn -A10 '^struct ' src/ --include='*.rs' | grep -E ':[ \t]*&[^'"'"'][^;]*,$'
Forbidden 6 — Cargo-Culted 'a: 'b Outlives Constraints
Forbidden: Adding 'a: 'b ("lifetime 'a outlives 'b") or T: 'a bounds copied
from a compiler suggestion without understanding why they are needed.
Why: 'a: 'b is a lifetime subtyping constraint — it says 'a is at least as long
as 'b. It is correct and necessary when a shorter-lived reference must hold a value
borrowed from a longer-lived region. Cargo-culting it causes over-constrained APIs that
reject valid callers. E0309 is the correct trigger: it fires when a T: 'a bound is
missing from a struct definition; it is fixed by adding the exact bound the compiler
requires, not by adding every outlives relationship in sight. Book: "Generic Type
Parameters, Trait Bounds, and Lifetimes" and error index E0309.
fn pick<'a: 'b, 'b>(x: &'a str, y: &'b str) -> &'b str { y }
fn pick<'a, 'b>(x: &'a str, y: &'b str) -> &'b str { y }
struct Wrapper<'a, T: 'a> {
inner: &'a T,
}
grep -rn "'"'[a-z]*:.*'"'"'[a-z]" src/ | grep -v '// @outlives-required'
Book References
Related Skills
- rust-ownership-borrowing — the ownership and borrowing rules that lifetimes enforce
- rust-traits-generics — combining trait bounds with lifetime parameters (
T: Display + 'a)
- rust-smart-pointers —
Arc/Rc and when owning data beats borrowing it across scopes
Source: adelabdelgawad/rust-fullstack-agents — distributed by TomeVault.