Chapter 05What a Lifetime Actually Is
We've been drawing lifetimes for four chapters without admitting it. Time to define the thing, learn the notation, and — most importantly — learn what the notation does not do.
A region of code, not a length of time
Despite the name, a lifetime isn't a duration measured on a clock. A lifetime is a region of the program — a set of points in the code — during which a borrow is valid and may be used. When the compiler checks the broken example from last chapter, it's comparing two regions:
fn main() {
let r; // ────────┐ the borrow's region 'a
{ // │
let x = 5; // ──┐ 'b │ x's region
r = &x; // │ │
} // ──┘ │ x ends; 'a keeps going…
println!("r: {r}"); // ────────┘ …because r is used here
}
The borrow stored in r needs to be valid over region
'a (through the
println!). The data it points at only exists over region
'b. Is
'b at least as big as
'a? No — so the program is rejected.
That's the entire algorithm, conceptually: the referent's region must
cover the reference's region. Rust spells “covers” as
outlives, written 'b: 'a
(“'b outlives 'a”).
Every &x your program creates has a lifetime, always —
you just usually don't write it. The full type of a reference is
&'a T or
&'a mut T:
“a reference to T that is guaranteed valid throughout region
'a.” The lifetime is part of the
type.
The notation: 'a
A tick mark and a short name: 'a,
'b, 'input —
the name is yours to choose, like a generic type parameter. In fact it
is a generic parameter. Compare:
fn largest<T>(list: &[T]) -> &T // generic over a TYPE
fn first<'a>(list: &'a [i32]) -> &'a i32 // generic over a REGION
largest works for any type T;
first works for any region
'a. When you call first,
the compiler infers a concrete region for
'a from the argument you pass, exactly
the way it infers a concrete type for T. Nobody writes
“lifetime arguments” at call sites; inference handles it. Your job is only
to describe, in signatures and type definitions, how the regions
relate.
What annotations do — and don't do
This is the single most important paragraph in the book, so let's give
it room. Lifetime annotations never change how long anything
lives. They don't extend borrows, they don't delay drops, they
don't allocate, they don't exist at run time at all — a
&'a str compiles
to the same bare pointer as a C char*. Annotations are
constraints in a proof: they tell the compiler how the lifetimes of
several references relate, so it can check that the relationships hold at
every call site.
The workshop version: writing a longer loan period on the borrow slip doesn't make the owner stay in the shop longer. It just changes what the steward will check. If the slip promises the tool until Friday but the owner leaves Tuesday, the steward rejects the slip — the slip never had the power to keep the owner around.
When the borrow checker complains, the reflex is to add or lengthen
lifetimes — 'static if desperate —
as if annotations were levers. They're not; they're claims. If the claim
is false, the error moves; it doesn't leave. The real fix is always in
the code's shape: restructure who owns what, or shorten who borrows what.
(Chapter 9 dissects the 'static
version of this trap.)
Where lifetime parameters may appear
Three places, and we'll spend a chapter on each family:
| Position | Example | Meaning |
|---|---|---|
| Function signatures | fn f<'a>(x: &'a str) -> &'a str |
How outputs borrow from inputs (ch. 6–7) |
| Types (structs/enums) | struct Excerpt<'a> { part: &'a str } |
This type contains borrows and can't outlive them (ch. 8) |
| Bounds | T: 'a, 'b: 'a, dyn Trait + 'a |
Outlives constraints on generics and trait objects (ch. 9–10) |
One more citizen of the notation: '_,
the anonymous lifetime. It says “there's a lifetime here, and I'm
letting the compiler pick it by its usual rules.” You'll see it in
impl Excerpt<'_>
and in return types like -> Excerpt<'_>,
where it flags — honestly and cheaply — that borrowing is happening without
naming names.
How big is a borrow's region, really?
One refinement before we move on. In old Rust (pre-2018), a borrow's region ran from creation to the end of its lexical scope — the closing brace, whether you were still using it or not. Modern Rust is smarter: a borrow's region runs from creation through its last use. This is called non-lexical lifetimes (NLL), and it's why this compiles today:
let mut s = String::from("hello");
let r1 = &s;
println!("{r1}"); // r1's region ends HERE (last use)
let r2 = &mut s; // ✓ no overlap, no conflict
r2.push_str("!");
Keep “region ends at last use” in your head as the default model; it explains most “why does this compile?” moments. The full story — and the places where even NLL says no — waits in chapter 10.
So what did we actually gain?
The fog around 'a should be lifting.
A lifetime is a region of code; it's part of a reference's type; it's a
generic parameter inferred at call sites; and annotating it describes
relationships without changing behavior. The check is interval
containment: data's region must cover borrow's region. Next we put the
notation to work where it earns its keep — function signatures.
Challenges
- In the NLL example, swap the last two lines
(
r2.push_strbeforelet r2— well, you can't; so instead moveprintln!("{r1}")to the end). What happens, and which bar in the interval picture grew? - True or false:
&'a strand&'b strare different types. Defend your answer using the generic-parameter analogy. - Explain to a rubber duck why adding
'staticto a failing signature can't fix a program whose data genuinely dies early.