Lifetime Annotations
Explicit lifetime annotations tell the compiler how reference parameters and return values relate. They appear in function signatures, structs, enums, and trait impls.
Recipe
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s = String::from("abcd");
let slice = longest(&s, "xy");
println!("{slice}");
}When to reach for this: Public APIs returning references, structs holding borrows, and resolving lifetime errors the compiler cannot elide.
Working Example
struct Parser<'a> {
input: &'a str,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Self { input }
}
fn peek_word(&self) -> Option<&'a str> {
self.input.split_whitespace().next()
}
}
fn main() {
let text = String::from("hello world");
let p = Parser::new(&text);
println!("{:?}", p.peek_word());
}What this demonstrates:
impl<'a>repeats lifetime on impl block- Methods return
&'a strtied to parser's input Parsercannot outlivetext- Same lifetime on all tied references
Deep Dive
Function Annotations
fn split<'a>(s: &'a str, at: usize) -> (&'a str, &'a str) {
s.split_at(at)
}Struct with Multiple Lifetimes
struct Pair<'a, 'b> {
first: &'a str,
second: &'b str,
}Each field may borrow from different sources.
Lifetime Bounds
fn copy_str<'a: 'b, 'b>(x: &'a str, y: &'b mut &str) {
*y = x;
}'a: 'b means 'a outlives 'b.
impl Lifetimes
Lifetime params on impl must match struct/trait definition when methods return borrowed inner data.
Gotchas
- Annotating only return lifetime - Inputs need connection. Fix: Add
'ato all related refs. - Unrelated lifetimes forced equal - Over-constrains API. Fix: Use separate
'a,'bwhen sources differ. - Missing lifetime on
implstruct with refs - Compile error on methods. Fix:impl<'a> Struct<'a>. - Lifetime on
fninsideimplwithout need - Noise. Fix: Use elision when compiler accepts. - Thinking
'asets duration - You declare relationships; compiler verifies callsites. Fix: Read errors as constraint failures.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Owned fields | No borrow coupling | Need zero-copy view |
Generic AsRef<str> | Accept many string types | Return type still borrows |
Indices + parent Vec | Self-referential graphs | Simple substring API |
Arc<str> | Shared immutable storage | Single-owner string |
FAQs
Must every reference have `'a` written?
No. Elision and inference handle many cases. Annotate when required.
Can lifetimes be named descriptively?
Yes: 'line, 'input. 'a is convention for first param.
What about `&mut` lifetimes?
Same rules. Exclusive borrow lifetime tied to owner.
Lifetime on trait methods?
Trait definitions may include lifetime params; impls must match.
Can I omit struct lifetime with single ref?
Sometimes with lifetime elision on structs (rare). Often explicit is clearer.
What is `'_` placeholder?
Instructs inference to fill lifetime in ambiguous positions: Parser<'_>.
Multiple refs returning one?
All inputs sharing output lifetime must live long enough - unify with 'a.
Annotations in closures?
Closures usually infer. Move closures may need explicit where on outer fn.
Do async fn lifetimes differ?
Return futures may capture lifetimes; prefer owned captures in async when possible.
Tooling help?
rust-analyzer shows required lifetimes in signature assists.
Related
- Lifetime Elision - when to omit
- Lifetimes Explained - conceptual model
'static& Bounds - outlives bounds- Lifetime Mismatch Scenarios - fixes
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.