Everyone’s first lifetime error arrives the same way. You write a function that returns a reference — say, the longer of two strings — the compiler answers error[E0106]: missing lifetime specifier, and you do what every beginner does: sprinkle 'a and 'static around the signature until the red squiggles stop. Sometimes it even compiles. This is exactly backwards, and it’s why lifetimes have their reputation: people treat the annotations as spells that change behavior, when they’re actually declarations the compiler checks. Adding 'static doesn’t make your data live longer, any more than declaring yourself taller changes your height.
Here’s the reframe that makes this part short: a lifetime is not a thing you control. It’s a region of your program where a reference is valid — and the annotation language exists so you can tell the compiler how those regions relate. Part 2 gave you the rules of borrowing; this part gives you the grammar for talking about them across function boundaries.
Annotations describe, they never command
Watch what the compiler actually needs in the classic example:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Why does this need an annotation at all? Because the caller needs to know how long the returned reference stays valid — and the answer must be computable from the signature alone, without reading the body. The function could return x or y, so the honest answer is: the result may live at most as long as the shorter of the two inputs. That’s all 'a says. It names a region — “whichever of x and y ends first” — and puts the output inside it.
Three consequences fall out, and they’re the whole mental model:
- Annotations change nothing at runtime. They’re erased after checking. Your program runs identically with or without them — they exist purely so the compiler can prove the borrows are sound.
- The output’s lifetime must trace to an input (or to
'static). A function can’t manufacture validity from nothing; if the signature can’t express where the output comes from, the design is wrong, not the syntax. - Wrong annotations get rejected too. “It compiles, so my annotations are correct” is a real misconception — you can write
'arelationships that are stricter than necessary and lose flexibility your callers wanted. The annotation is a contract; sign it carefully.
You write fewer of these than you think: elision
If every reference needed an annotation, Rust would be unreadable. So the compiler fills in the three cases that are unambiguous — the lifetime elision rules:
- Every input reference gets its own lifetime parameter.
fn f(x: &i32, y: &i32)is secretlyfn f<'a, 'b>(x: &'a i32, y: &'b i32). - If there’s exactly one input lifetime, it’s assigned to the output.
fn first_word(s: &str) -> &strjust works — one input, one answer, nothing to decide. - In methods,
&self’s lifetime is assigned to the output.fn name(&self) -> &strties the result to the object, which is almost always what you meant.
The crucial property: the compiler never guesses. If all three rules fire and the output’s lifetime is still undetermined — longest, with two inputs and no self — it stops with E0106 and asks you. That’s not the compiler being weak; it’s the compiler refusing to sign a contract it can’t read. Most of your functions land in rules 2 and 3, which is why Rust feels annotation-free right up until the moment it very much isn’t.
The two places you must annotate
Elision covers functions. Everything else is on you — and “everything else” is basically two shapes:
Functions where the output’s source is ambiguous — like longest. The fix is always the same move: name the relationship. When inputs genuinely have different lifetimes and the output only depends on one, say that instead:
fn extract<'a>(ctx: &'a Context, _pattern: &str) -> &'a str {
ctx.data // outlives pattern? irrelevant — it's tied to ctx
}
Structs that hold references — a struct field of type &'a str means the struct is a borrowed view of something else, and the struct instance can never outlive its source:
struct Parser<'a> {
input: &'a str, // this Parser borrows; it cannot own
}
This is where beginners meet the wall: you cannot return a Parser<'a> from a function that created the source string inside itself — the source dies at the closing brace, and the compiler says so (E0515). That’s not a lifetime problem, it’s an ownership fact wearing a lifetime costume, and the fix is usually upstream: pass the source in as a parameter, or store an owned String instead. Knowing which fix the situation wants is most of the skill.
’static wears two hats
The most-misunderstood lifetime is the one that sounds the simplest. 'static means two different things in two different positions, and confusing them is the classic detour:
&'static Tis a place — a reference to data that lives for the entire program: string literals,staticitems, deliberately leaked allocations. This is genuinely rare, which is why “make it'static” is almost never the right fix.T: 'staticis a property — a bound meaning “this type contains no borrowed references.” And here’s the part that surprises everyone: all owned data satisfies it. AStringis'static. AVec<u8>is'static. Whenstd::thread::spawndemandsF: 'static, it’s not asking for immortal data — it’s asking for data that borrows nothing, so the closure can outlive whoever spawned it.move ||an ownedStringin there and you’re done. NoBox::leak, no ritual sacrifice.
The 2024 edition quietly fixed your future errors
Two lifetime-adjacent changes in the 2024 edition are worth knowing before they save you an hour each. Tail-expression temporaries now drop before local variables — the famous RefCell case (c.borrow().len() as a function’s last line) that 2021 rejected now compiles. And if let temporaries now drop before the else branch — fixing a genuine deadlock pattern where a read lock held through the scrutinee met a write lock in the else.
The flip side of the same change: tail-expression temporaries may now die earlier than 2021 allowed, so let x = { &String::from("1234") }.len(); — which 2021’s temporary-extension rules rescued — fails in 2024. The fix is the habit you’ll want anyway: lift temporaries into named let bindings. A named binding has a name, a scope, and no surprises.
Practice, then Part 4
- Write
longestwithout annotations and read E0106 fully. Add'aas above, then call it with two strings of deliberately different scopes and confirm: the result dies with the shorter one. Print it after the short one drops and watch the compiler refuse. - Take the
extractexample and remove_pattern’s independence: change the body to sometimes return_pattern. Watch the compiler force you to unify both inputs under'a— feel the contract tightening, exactly as it should. - Do the
'staticdrill: spawn a thread with amoveclosure capturing an ownedString(compiles), then try capturing&String(doesn’t — it borrows). Now you know whatT: 'staticactually asks for, and you’ll neverBox::leakout of confusion again.
Part 4 moves from how long things live to what things are: enums as state machines, pattern matching as the control flow Rust actually wants, and error handling with Option, Result, and the ? operator — the type system’s first real payoff.