rgoussu@goussu: ~/library/rust/ownership-and-concurrency
~/library/rust/ownership-and-concurrency cat lifetimes.md

Lifetimes — elision, 'static, variance & the craft

# What lifetime annotations actually are, the elision rules that hide them, structs holding references, the two meanings of 'static, variance and HRTBs, and when to restructure instead of annotate.

Conceptsaved 2026-08-09 #rust#lifetimes#borrow-checker#api-design

Overview

A lifetime is a name for a region of code during which a reference is valid — nothing runs at runtime, nothing is allocated, and annotations never change how long anything lives. They exist purely so function and type signatures can state relationships the borrow checker then verifies: "the return value borrows from the first argument", "this struct may not outlive the string it points into". Most lifetimes are elided; the craft is knowing what the elision rules wrote for you, reading 'a as the relationship it declares, and recognizing when a lifetime tangle is the design asking to own its data.

Key points

  • Annotations describe, never prescribe: 'a doesn't make anything live longer — it's a variable the checker solves for. If there's no valid solution, the caller's code is what gets rejected; the signature is a contract, and lifetime parameters are part of a function's API exactly like types.
  • The three elision rules write most signatures for you: each reference parameter gets its own lifetime; if there's exactly one input lifetime it's assigned to all outputs; if &self/&mut self is present, its lifetime goes to the outputs. Explicit annotation is needed precisely where these leave ambiguity — multiple reference inputs feeding a reference output.
  • Structs holding references infect their users: struct Parser<'a> { input: &'a str } declares "this value may not outlive the input it borrows" — correct for short-lived views (parsers, iterators), viral for long-lived state. A 'a on a struct stored in another struct stored in an app context is the classic sign the field should own its data (String, or Cow<'a, str> to defer the choice).
  • 'static means two different things: as a borrow (&'static str — lives for the whole program: literals, leaked boxes) and as a bound (T: 'static — the type contains no non-static borrows, i.e. "owns its data"). The second is what thread::spawn and tokio::spawn demand, and misreading it as the first causes the "does my value have to live forever?!" panic — it doesn't; it has to be ownable.
  • Variance is why it mostly just works: a longer-lived reference coerces to a shorter-lived one (&'static str where &'a str is wanted) because &'a T is covariant in 'a; &'a mut T is invariant in T, which is why some &mut signatures are stricter than intuition expects. File under "know it exists" — the Rustonomicon has the full table.
  • HRTBs (for<'a> Fn(&'a str) -> &'a str) quantify over all lifetimes — the checker writes them for you on most closure bounds; you write them by hand mainly for function-pointer/closure APIs that borrow their arguments.
  • GATs (stable 1.65) let a trait's associated type carry a lifetime — type Item<'a> — unlocking lending iterators (yield borrows into self) and collection-agnostic APIs that were previously unwritable.
  • Restructure beats annotate: chasing a third explicit lifetime through a signature is the smell; the fixes are owning the data, splitting the type, indices over references, or narrowing the borrow's scope. Annotations are for stating simple relationships precisely — complex relationships belong in the design.

Examples

// Elision leaves this ambiguous — annotation states the relationship:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

// T: 'static — "owns its data", not "lives forever":
fn spawn_worker<T: Send + 'static>(job: T) { /* thread::spawn(move || …) */ }
let owned = String::from("fine");     // moved in: satisfies 'static
// let borrowed = &local;             // a &'a String would not

// Cow — defer the borrow-vs-own decision to runtime:
fn normalize(input: &str) -> std::borrow::Cow<'_, str> {
    if input.contains('\r') { Cow::Owned(input.replace('\r', "")) }
    else { Cow::Borrowed(input) }
}

Related

Citations

[1] The Book, ch. 10.3 — Validating References with Lifetimes [2] The Rustonomicon — Subtyping and Variance [3] The Rust Reference — Lifetime elision