Rust Lifetimes Contents Cheatsheet

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”).

The Key Idea

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.

Common Trap

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.)

fn main() { … } 'owner — region where the data exists 'a — region where the borrow is used legal ⇔ 'owner covers 'a — written 'owner: 'a (“outlives”)
The outlives relation is interval containment: the teal bar must cover the rust one.

Where lifetime parameters may appear

Three places, and we'll spend a chapter on each family:

PositionExampleMeaning
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

  1. In the NLL example, swap the last two lines (r2.push_str before let r2 — well, you can't; so instead move println!("{r1}") to the end). What happens, and which bar in the interval picture grew?
  2. True or false: &'a str and &'b str are different types. Defend your answer using the generic-parameter analogy.
  3. Explain to a rubber duck why adding 'static to a failing signature can't fix a program whose data genuinely dies early.
⌂ Library