Here is the single most common way people get stuck on lifetimes: they write
Rust happily for two weeks, hit an error, paste <'a> into a signature until
the compiler goes quiet, and from that day forward believe that lifetimes are a
strange advanced feature that occasionally has to be appeased.
That belief is wrong in a specific, fixable way. Lifetimes were never switched on. They were there the whole time.
The function you have already written
Consider a function you would not think twice about:
fn trim(s: &str) -> &str {
s.trim()
}
No lifetimes anywhere. Except that is not what the compiler sees. What the compiler sees is:
fn trim<'a>(s: &'a str) -> &'a str {
s.trim()
}
Both forms are the same function. The first is sugar for the second. The
compiler filled in the annotation by a mechanical rule (which gets its own
article) and moved on. Every &T you have ever typed had a lifetime attached
to it; you just did not have to say which one.
This matters because of what it implies about the day the compiler does ask you. It is not introducing a new obligation. It is telling you that this time, the mechanical rule ran out of information — and it needs you to supply the one fact it cannot infer.
💡If fn trim(s: &str) -> &str already means fn trim<'a>(s: &'a str) -> &'a str, why does the compiler ever need you to write the annotation by hand?
click to reveal
Because the rule that fills them in is deliberately simple, and simple rules have blind spots.
Filling in the input side is always unambiguous — every reference in the parameter list gets its own fresh lifetime, no thinking required. The output side is where information runs out. If a function takes exactly one thing that carries a lifetime, there is only one candidate for the output, so the compiler picks it. If it takes two, there are two candidates, and the compiler does not guess:
fn pick(a: &str, b: &str) -> &str { .. } // from a? from b? from either?
Only you know whether the returned slice is carved out of a, out of b, or out of whichever one is longer. That is a fact about your intent, not about your code’s text, and no amount of compiler cleverness recovers it from the signature. So it asks.
The deep point: the annotation is not there to make the program work. It is there because a function signature is a contract with callers, and the compiler refuses to invent a clause of that contract on your behalf.
Vocabulary, and one sentence you must stop saying
Almost every persistent misconception about lifetimes traces back to a single sloppy phrase: “the lifetime of a variable.” There is no such thing, and believing there is will cost you weeks.
Here are the four words you actually need, used precisely:
-
Referent — the value being pointed at. In
let r = &s;, the referent iss. -
Borrow — the act of taking a reference, and by extension the reference
itself.
&sborrowss. -
Borrower — whoever holds the reference. Above, that is
r. - Region (or lifetime) — the set of program points over which a particular reference is required to be valid.
And the distinction:
A variable has a scope. A reference has a lifetime.
A variable’s scope is a syntactic fact about your source code: it starts where
the let is and ends at the closing brace of the enclosing block. It is about
names. You can read it off the page.
A reference’s lifetime is a region the borrow checker computes: the set of points in the program where that reference is used, and therefore must still be valid. It is about validity. It is often much smaller than the scope of the variable holding it, and it is not brace-shaped.
fn main() {
let text = String::from("hello world");
let first = &text[..5]; // `first` is a borrower
// its referent is `text`
println!("{first}"); // last use of the borrow
// -- the borrow's REGION ends about here
let mut owned = text; // moving `text` is now fine
owned.push('!');
println!("{owned}");
}
first the variable is in scope right down to the closing brace. The
borrow it holds has a region that ends after the println!, which is exactly
why moving text on the next line is allowed. If lifetimes were scopes, that
program would not compile. It does, on every Rust since 2018.
💡Rewrite this in the precise vocabulary: "first lives until the end of main, so text is borrowed until the end of main."
click to reveal
Something like: “The variable first is in scope until the end of main, but the borrow it holds has a region ending at its last use — the println!. After that point text is no longer borrowed.”
Notice what changed. The original sentence conflates three different things — a variable’s scope, a reference’s region, and the referent’s validity — and gets a wrong conclusion out of the confusion. The rewrite keeps them apart and gets the right one.
This is not pedantry for its own sake. The rewritten sentence tells you the move on the next line is legal; the original one tells you it is not. One of them predicts what the compiler does.
What a lifetime is for
The borrow checker enforces exactly one promise:
A reference must never outlive its referent.
Everything else — every 'a, every error code in this track, every argument
about elision — is bookkeeping in service of that one sentence. A lifetime is
how the compiler tracks, for each reference, the region over which that promise
must hold, so it can check the promise is kept.
Two consequences follow immediately, and both surprise people:
Lifetimes are entirely a compile-time device. They are erased before code
generation. There is nothing in the compiled binary that corresponds to 'a —
no tag, no counter, no runtime check. A reference at runtime is a pointer, full
stop. This is why Rust’s memory safety is free: the checking happened before
the program existed.
You cannot test a lifetime at runtime. No assertion, no unit test, no
amount of clever input can distinguish a function with correct lifetime
annotations from one with sloppy-but-accepted ones. The compiler is the only
grader that exists. (Which is why the problems in this track pin your
signatures with little const _: declarations — those are compile-time
tests, and they are the only kind available.)
💡A colleague says: "I added 'static to that reference so the data lives longer and stops getting dropped early." What has gone wrong in their model?
click to reveal
Everything, and it is worth being blunt about it because this exact sentence is the number-one wrong mental model in the language.
Annotations do not extend anything. They are not allocation, not reference counting, not a retain call. They are descriptions of relationships the compiler then checks. Writing 'static on a reference does not make the referent live longer; it asserts that the referent already lives for the whole program, and if that assertion is false the program is rejected — usually with a worse error than the one they started with.
The mental move that fixes it: stop reading 'a as a duration (“how long this lives”) and start reading it as a constraint (“this must be valid at least everywhere this is used”). A constraint can be satisfied or violated. It cannot be granted.
That reframe is important enough that it gets the whole next article.
Where this is going
For the rest of this track, hold on to three things:
- Every reference has a lifetime. You have been writing them all along.
- A variable has a scope; a reference has a lifetime. Never mix the words.
- An annotation describes a relationship. It never changes when anything is created or destroyed.
Next: why “a lifetime is a duration” is the model that breaks everything, and what to replace it with.