Introduction
When the compiler cannot infer lifetime relationships on its own, you supply explicit lifetime annotations. Rust also has three elision rules that automatically insert lifetimes in common patterns, so most function signatures remain clean and annotation-free.
Key Concepts
- Lifetime parameter: A generic parameter starting with
'(e.g.,'a,'b) that names a particular lifetime scope. - Lifetime elision: A set of compiler rules that automatically assign lifetime parameters to function signatures in predictable patterns.
- Struct lifetimes: When a struct holds a reference, it must declare the lifetime of that reference so the borrow checker can track it.
Real World Context
In production Rust code, you rarely see explicit lifetimes on simple functions because elision handles them. But the moment you write a function that returns a reference derived from multiple inputs, or a struct that borrows data, you need to understand the annotation syntax.
Deep Dive
Lifetime parameters are declared in angle brackets and applied to references:
rust// Both inputs and output share the same lifetime fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
This says: "The returned reference will live at least as long as the shorter of x and y." When you need distinct lifetimes because the output only depends on one input, use separate parameters:
rustfn first_word<'a, 'b>(s: &'a str, _other: &'b str) -> &'a str { s.split_whitespace().next().unwrap_or(s) }
The compiler applies three elision rules in order:
- Each input reference gets its own lifetime:
fn foo(x: &i32, y: &i32)becomesfn foo<'a, 'b>(x: &'a i32, y: &'b i32). - If exactly one input lifetime, it is assigned to all outputs:
fn foo(x: &i32) -> &i32becomesfn foo<'a>(x: &'a i32) -> &'a i32. - If there is
&selfor&mut self, its lifetime is assigned to outputs:fn method(&self) -> &strbecomesfn method<'a>(&'a self) -> &'a str.
Structs that hold references must declare the lifetime:
ruststruct Excerpt<'a> { part: &'a str, } impl<'a> Excerpt<'a> { // Elision rule 3: output gets lifetime of &self fn get(&self) -> &str { self.part } }
RPIT Lifetime Capture (2024 Edition)\n\nRPIT stands for Return Position Impl Trait — the impl Trait syntax in function return types.
In the 2024 edition, impl Trait in return position now captures all in-scope generic and lifetime parameters by default. In previous editions, lifetime parameters were not captured unless explicitly mentioned:
rust// 2021 edition: 'a is NOT captured — compiles // 2024 edition: 'a IS captured — may cause borrow errors fn make_iter<'a>(slice: &'a [i32]) -> impl Iterator<Item = &i32> { slice.iter() }
If you need to opt out of capturing specific lifetimes in the 2024 edition, use the use<> syntax (stabilized in Rust 1.82):
rust// Explicitly capture only 'a, not other in-scope lifetimes fn make_iter<'a, 'b>(a: &'a [i32], _b: &'b str) -> impl Iterator<Item = &'a i32> + use<'a> { a.iter() }
This change makes lifetime capture more consistent with how generic type parameters were already captured.
Common Pitfalls
- Adding unnecessary lifetime annotations — If elision handles the signature, adding explicit lifetimes is noise. Only annotate when the compiler requires it.
- Confusing lifetime parameters with concrete lifetimes —
'ais not a fixed scope; it is a placeholder that the compiler fills in at each call site based on actual borrow scopes. - Unexpected captures in the 2024 edition — When migrating from the 2021 edition,
impl Traitreturn types may capture more lifetimes than before, causing new borrow checker errors. Use+ use<>to restrict captures.
Best Practices
- Learn the three elision rules — Knowing when the compiler can infer lifetimes helps you write cleaner signatures and understand error messages.
- Use descriptive lifetime names in complex signatures — For functions with many lifetime parameters, names like
'srcand'destare clearer than'aand'b. - Use
+ use<>for precise control — In the 2024 edition, when animpl Traitreturn type should not capture all in-scope lifetimes, use the explicituse<>syntax.
Summary
- Lifetime annotations use the
'asyntax and describe how reference lifetimes relate. - Three elision rules automatically insert lifetimes in most function signatures.
- Structs holding references must declare the lifetime parameter.
- In the 2024 edition,
impl Traitreturn types capture all in-scope lifetimes by default — use+ use<>to opt out. - Only add explicit annotations when the compiler cannot infer them.
Code Examples
// Struct with lifetime
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
// Elision rule 3: output gets lifetime of &self
fn get(&self) -> &str {
self.part
}
// Explicit when returning something else
fn with_prefix(&self, prefix: &str) -> String {
format!("{}{}", prefix, self.part)
}
}