Lifetimes
The ‘a sticker
You’ve been seeing references since the ownership chapter — &T, &mut T,
&str. Every one of those references has a hidden tag attached to it: a
lifetime. The lifetime is just a name for “how long this reference is
allowed to point at the thing it points at.”
Most of the time the compiler figures the lifetime out for you and you never
see it. But occasionally — when a function returns a reference, or a struct
holds one — you have to write the lifetime out by hand so the compiler can
verify your math. That’s what 'a is. It’s a sticker the compiler can read.
Lifetimes scare people because of how they look. They are, secretly, the most boring concept in Rust. You’ll learn three rules and then never have to think about it again for the rest of your career.
Why we need them at all
Consider this function. It takes two string slices and returns whichever one is longer.
Run that. The compiler complains:
missing lifetime specifier… this function’s return type contains a borrowed value, but the signature does not say whether it is borrowed from
aorb
The compiler needs to know: when the caller hangs onto the returned &str,
which input is it actually borrowing from? If the answer is “could be
either,” then the returned reference must live no longer than the shorter
of the two inputs.
We tell it that with a lifetime annotation:
The <'a> introduces a lifetime parameter named a. We then stamp it on both
inputs and on the output. The signature now reads, in English: “there exists
some lifetime 'a; both inputs must live at least that long, and the output
will live exactly that long.”
The compiler picks the actual concrete lifetime when you call the function — it picks the shortest one that satisfies the constraints.
Lifetimes elision — the rules you don’t have to write
If lifetimes were always required, Rust code would be a sea of 'as. They’re
not, because of three lifetime elision rules the compiler applies to
function signatures:
- Every input reference gets its own implicit lifetime parameter.
- If there’s exactly one input lifetime, it’s assigned to all output references.
- If one of the input references is
&selfor&mut self, its lifetime is assigned to all output references.
Those three rules cover the vast majority of real-world functions, so you
almost never write 'a for input/output references. Watch:
Not a single explicit lifetime, and yet every reference is correctly tracked. The elision rules are doing the work behind the scenes.
You only write a lifetime when the elision rules can’t figure it out — which in practice means: a function with multiple input references that returns a reference, where the compiler can’t tell which input the output is borrowed from.
Lifetimes on structs
If a struct holds a reference, the struct itself needs a lifetime parameter so the compiler can verify the borrowed thing outlives the struct.
The <'a> after the struct name declares “this struct has some lifetime
parameter I’m going to call 'a.” Every reference field with that lifetime
must outlive the struct itself. The compiler enforces this automatically.
If you tried to make sentence go out of scope before h, the compiler would
gently scream at you.
The 'static lifetime — “lives forever”
There’s one special lifetime, 'static, which means “lives for the entire
program.” String literals get this lifetime automatically — they live in the
binary’s read-only data section and are never freed.
You’ll see 'static when:
- A function returns a literal or other compile-time constant string.
- You’re working with constants:
const FOO: &'static str = "…"; - You’re sending data between threads (we’ll talk about why in the concurrency chapter).
You should not use 'static to “make the borrow checker shut up.” That’s a
trap; it usually means you’ve got an ownership problem you haven’t solved.
Real 'static data is rare.
A worked example — the borrow that has to end
Here’s a classic situation that trips people up:
The return type (&str, &str) has two references. Why doesn’t the compiler
need lifetime annotations here? Elision rule 2: there’s only one input
lifetime (&str), so both output references inherit it. Both a and b
borrow from sentence, and the compiler verifies that sentence outlives the
println.
Now try this variation — note carefully what’s different:
fn pick_one(a: &str, b: &str) -> &str { if a.len() < b.len() { a } else { b }}Two input references, neither one is &self. Elision can’t decide. You have
to write the lifetime out (as we did at the top of the chapter). The compiler
isn’t being mean — it literally has no way to know whether the output borrows
from a or from b.
A foot-gun and how to spot it
The most common lifetime error in real code:
That compiles and runs fine — elision handles it. But watch what happens if you try to make it work across function boundaries with the wrong shape:
fn longest_string() -> &String { // 💥 — borrowed from where? let v = vec![String::from("oops")]; &v[0]}You can’t return a reference to a local. v would be dropped at function
exit; the reference would dangle. The compiler refuses, with one of its
classic error messages: “returns a reference to data owned by the current
function.” The fix is to return an owned String, not a &String.
What you take away
- Every reference has a lifetime; most of the time the compiler infers it.
- You write
'awhen there are multiple input references and the compiler can’t tell which one the output borrows from. - Structs with reference fields need a lifetime parameter so the compiler can ensure the struct doesn’t outlive its data.
'staticmeans “lives for the whole program” — rare, often a sign that you meant to own data, not borrow it.
The thing nobody tells you about lifetimes: once you grok them, they
disappear. You’ll spend ten minutes a year writing 'a. The other 99.99% of
the time, the compiler quietly does it for you.
Onward to smart pointers, where we start building data structures whose ownership isn’t a single tree.