Part 2 · The compile-time machine I — borrows & lifetimes
Reading lifetime annotations
The practical half. Elision rules, the anonymous lifetime, the two meanings of `static`, and enough variance to follow the conversation.
What this lesson is trying to break (3)
- Most functions have no lifetimes because they do not need any.
- `T: static` means the value lives for the whole program.
- If two things have the same lifetime name, they must be equally long-lived.
Lesson 2.3 was the model. This one is the reading practice: what you actually see in signatures, and how to decode it fast.
Why most functions show no lifetimes
They have them. They are hidden.
fn first_word(s: &str) -> &str { /* ... */ }
The compiler expands this to:
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
That expansion is lifetime elision, and it runs on exactly three rules. Learn them once; they explain nearly every signature you will read.
Varies: the number and kind of input references
| Rule | When it applies | Effect |
|---|---|---|
| 1 | Always, to inputs | Each elided input lifetime gets its own fresh parameter. |
| 2 | Exactly one input lifetime | That lifetime is given to all elided output lifetimes. |
| 3 | Several inputs, one of which is &self or &mut self | self’s lifetime is given to all elided output lifetimes. |
If no rule determines an output lifetime, elision fails and the compiler demands an explicit annotation. That error is the compiler saying it cannot guess which input the output borrows from.
Rule 3 is why methods almost never need annotations — and it encodes a real
assumption: if a method returns a reference, it probably borrows from self,
not from the arguments. That is right the overwhelming majority of the time.
Now watch elision fail:
fn longest(x: &str, y: &str) -> &str { /* ... */ }
Rule 1 gives x and y separate lifetimes. Rule 2 does not apply — there are
two. Rule 3 does not apply — no self. So the output lifetime is undetermined:
error[E0106]: missing lifetime specifier
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`
Read that error as “tell me which input the output borrows from”. That is all
it wants. And it is exactly why longest needs the annotation while
first_word does not — not because one is more dangerous, but because one is
ambiguous.
What does elision expand this to?
impl Parser {
fn parse<'x>(&self, input: &'x str) -> &str { /* ... */ }
}fn parse<'s, 'x>(&'s self, input: &'x str) -> &'s strRule 3 wins: with a &self present, the elided output lifetime comes from
self, not from input — even though input has an explicit name and looks
like the more likely source.
This is worth knowing because it is a genuine trap. If parse actually wants to
return a slice of input — which is what a parser usually does — this signature
is wrong, and the error will appear inside the function body rather than at the
signature, complaining about lifetimes in a way that seems unrelated.
The fix is to say what you mean: -> &'x str.
Whenever a lifetime error inside a method body confuses you, check whether rule
3 has silently tied the output to self.
The anonymous lifetime '_
Foo<'_> · impl Trait for Foo<'_> · fn f(x: &str) -> Bar<'_>
“There is a lifetime here; infer it.” '_ does not name a region — it marks
the presence of one.
The value is honesty. Writing bare Foo when Foo has a lifetime parameter
hides the fact that the type borrows at all. Foo<'_> makes it visible while
staying silent about which region. The elided_lifetimes_in_paths lint exists to
push code in this direction, and modern crates mostly follow it.
When you see '_ in a return type, read: this return value borrows from
something in the arguments.
'static, both of them
This is the highest-value item in the lesson. 'static means two different
things depending on position, and mixing them up is the most common lifetime
error among people who otherwise know what they are doing.
Varies: only where the 'static appears
| Form | Meaning | Example that satisfies it |
|---|---|---|
&'static Ta reference type | This reference is valid for the entire program. The data is in the binary or was deliberately leaked. | "hello" (type &'static str), Box::leak(b) |
T: 'statica bound | This type contains no borrowed data with a shorter region. It says nothing about how long any value lives. | String, i32, Vec<u8>, &'static str — but not &'a str |
A `String` created and dropped in one line satisfies `T: 'static`. Nothing about that is long-lived. The bound is about *borrowing*, not about *duration*.
The example that makes it concrete:
tokio::spawn(async move { /* ... */ });
spawn requires F: Future + Send + 'static. That is the bound form. It
means: this future must not borrow anything from the stack frame that spawned
it — because the task may still be running after that frame returns.
It does not mean the future must live for the whole program. Most spawned tasks finish in milliseconds.
So when the compiler tells you `x` does not live long enough ... requires `'static` inside a spawn, the fix is never “make it live longer”. It is:
move the data into the task (async move), or clone it, or wrap it in an Arc.
That is one of the most common questions asked by people new to async Rust, and
knowing why is a genuine credibility marker.
Lifetimes on structs
struct Excerpt<'a> {
part: &'a str,
}
Read this as: this struct borrows, and it cannot outlive what it borrows
from. The 'a is not describing the struct — it is a tag saying “there is a
reference inside me, and here is the region it depends on”.
The practical consequence is the one that catches people building larger
programs: a struct with a lifetime parameter cannot be stored just anywhere.
It cannot be put in a 'static global, returned from a function whose inputs it
does not borrow from, or sent to a tokio::spawn. The lifetime propagates
outward through every type that contains it.
This is why experienced Rust programmers often reach for owned data
(String instead of &'a str) in struct fields even at the cost of an
allocation. Not laziness — a deliberate trade of a small runtime cost against a
large amount of lifetime propagation through the codebase. Being able to explain
that trade-off is a design-review skill.
Higher-ranked bounds
for<'a> Fn(&'a str) -> &'a str · F: for<'de> Deserialize<'de>
“Must work for every region, not one the caller picks.”
Ordinary generic lifetimes are chosen by the caller. A higher-ranked bound reverses that: the callee will supply references whose regions it invents, and your closure must handle all of them.
You meet it in exactly two places, and then constantly:
- closures that receive a reference the callee creates — a callback given a borrowed line from a file, say, where the region is per-iteration
serde—DeserializeOwnedis defined asfor<'de> Deserialize<'de>, meaning “deserialisable from a buffer of any lifetime”
Most of the time this is elided too: Fn(&str) -> &str already means
for<'a> Fn(&'a str) -> &'a str.
Variance, at reading level
You do not need the full theory. You need enough to follow a conversation and to recognise one error.
Covariant means a longer region can substitute for a shorter one. &'a T is
covariant in 'a, which is why you can pass a &'static str to something
expecting &'a str. This is the case you rely on constantly without noticing.
Invariant means no substitution at all — the regions must match exactly.
&'a mut T is invariant in T, and so are Cell<T> and RefCell<T>.
The one error this explains:
fn set_static(v: &mut Vec<&'static str>) { }
fn main() {
let s = String::from("x");
let mut v: Vec<&str> = vec![&s];
set_static(&mut v); // error — lifetime may not live long enough
}
Passing &mut Vec<&'a str> where &mut Vec<&'static str> is expected is
rejected even though 'static is “longer”, because &mut is invariant in its
contents. If it were allowed, the function could push a &'static str into the
vector — fine — but it could equally hand out the vector’s short-lived contents
as 'static. Invariance blocks both directions at once.
You will not write variance annotations; there are none to write. You just need
to recognise the phrase when a lifetime error mentions it, and know that the
answer is usually “because &mut is invariant”.
A colleague is stuck. They wrote:
async fn handle(cfg: &Config) {
tokio::spawn(async move {
use_config(cfg).await;
});
}and the compiler says the borrow may not live long enough — it requires
'static.
Explain why, using the bound meaning of 'static, and give them two
different fixes with the trade-off of each.
What you should now be able to say
- Three elision rules; rule 3 ties elided output lifetimes to
self, which is usually right and occasionally a trap. '_means there is a lifetime here, infer it — and signals that a value borrows.&'static Tis a reference valid for the whole program.T: 'staticmeans no borrowed data. Different things.- A lifetime on a struct propagates outward through everything containing it, which is why owned fields are often the right call.
for<'a>means “for every region”, and you meet it in closures andserde.&mutis invariant, which is the answer to a whole family of confusing errors.
Next: 2.5 — The borrow checker’s errors as a curriculum, which turns the eight errors you will actually hit into a diagnostic table.