Lifetimes: The Scope of Validity
Lifetimes have a reputation as the hard part of Rust, and the reputation is
mostly a misunderstanding about what they are. The misunderstanding is that a
lifetime annotation does something — that writing 'a creates or extends or
manages the life of a value, the way new and free do in C.
It does not. Every reference in every Rust program already has a lifetime, whether or not anyone wrote it down, and that lifetime was determined by the structure of the code long before the annotation was typed. A lifetime parameter in a signature is a description: it tells the compiler how the lifetimes of the things going in relate to the lifetimes of the things coming out. It is closer to a type annotation than to a memory operation, and like a type annotation, it is often unnecessary because the compiler can work it out.
Most Rust code contains no lifetime annotations at all. The chapters you have
read so far are full of references and free of 'a. What this chapter is about
is the model underneath, so that when you do meet an annotation you can read it,
and when the compiler tells you something does not live long enough, you can hear
the design question in it.
What a lifetime actually is
A lifetime is a region of the program: the span during which a reference is guaranteed to point at something valid. The compiler computes one for every reference and checks a single property, which is that the region a reference is used in fits inside the region the referent exists in.
Here is that check failing:
fn main() {
let outer;
{
let inner = String::from("temporary");
outer = &inner;
}
println!("{outer}");
}
The compiler says inner does not live long enough. It is not being pedantic
about a syntax rule. inner is dropped at the closing brace, its buffer is
freed, and outer would be a pointer into freed memory — the C bug from chapter
3, arriving without a single unsafe or raw pointer in sight. Lifetime checking is
that comparison, applied everywhere references travel.
Nothing in that example needed an annotation. The compiler could see both scopes. Annotations become necessary only at the one place where the compiler cannot see: a function boundary.
Why function signatures need help
A function is compiled once and called from anywhere. When the compiler checks a call site, it does not look inside the function body; it looks at the signature. So the signature has to carry enough information to answer the question the caller cares about, which is how long the returned reference stays valid.
Consider a function that returns whichever of two strings is longer. Written without annotations, it is rejected:
#![allow(unused)]
fn main() {
fn longer(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
}
The compiler’s complaint is that it cannot tell whether the return value borrows
from a or from b. That matters: if the result borrows from a, the caller
may drop b immediately and keep using the result, and if it borrows from b,
they may not. The body knows the answer is “either one, depending”. The signature
has to say so:
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let title = String::from("the rust thought process");
let subtitle = String::from("a book");
println!("{}", longer(&title, &subtitle));
}
Read the signature as a sentence: for some region 'a, given two string slices
that are both valid for 'a, I return a slice that is valid for 'a. Since the
caller supplies the arguments, 'a ends up being the shorter of the two
borrows, and the result cannot outlive either input. That is precisely the
guarantee the body needs.
Now look at a variant that tells a different story:
fn with_prefix<'a>(text: &'a str, prefix: &str) -> &'a str {
if text.starts_with(prefix) {
&text[prefix.len()..]
} else {
text
}
}
fn main() {
let path = String::from("/usr/local/bin");
let stripped;
{
let prefix = String::from("/usr");
stripped = with_prefix(&path, &prefix);
}
println!("{stripped}");
}
Only text carries 'a. The signature says the result borrows from the first
argument and has nothing to do with the second, which is why main can drop
prefix and go on using the result. Had both parameters shared 'a, that
program would not compile, and the restriction would be a fiction invented by a
careless signature rather than a fact about the code.
This is the thing to take from the chapter. A lifetime annotation is a claim about which inputs the output borrows from, and getting it right is a design decision, not a syntax ritual. The compiler will accept an over-strict signature happily; your callers are the ones who pay.
The cases you never have to annotate
Rust applies three rules before asking you for anything, and between them they cover the overwhelming majority of functions.
Every elided input lifetime becomes its own distinct parameter. A function taking two references is treated as taking two unrelated ones.
If there is exactly one input lifetime, it is assigned to every output lifetime.
This is why fn first_word(text: &str) -> &str from chapter 3 needs no
annotation: there is only one place the result could have come from.
If one of the inputs is &self or &mut self, its lifetime is assigned to every
output lifetime. This is why methods returning references almost never carry
annotations — the answer is nearly always “it borrows from the receiver”, and the
language assumes so.
When none of the three produces an unambiguous answer, the compiler stops and
asks, which is what happened with longer. Treat that as the prompt to decide
which input the result actually comes from.
Note
If you find yourself adding a lifetime parameter because the compiler asked and you are not sure which input is right, stop and work it out rather than trying
'ain different positions until it builds. A signature that compiles is not necessarily the signature you meant, and the wrong one becomes a constraint on every caller.
Types that borrow
A struct can hold a reference, and when it does, it takes a lifetime parameter:
struct Tokenizer<'a> {
input: &'a str,
position: usize,
}
impl<'a> Tokenizer<'a> {
fn new(input: &'a str) -> Tokenizer<'a> {
Tokenizer { input, position: 0 }
}
fn next_token(&mut self) -> Option<&'a str> {
let rest = &self.input[self.position..];
let start = rest.find(|c: char| !c.is_whitespace())?;
let remainder = &rest[start..];
let end = remainder.find(char::is_whitespace).unwrap_or(remainder.len());
self.position += start + end;
Some(&remainder[..end])
}
}
fn main() {
let text = String::from("ownership borrowing lifetimes");
let mut tokenizer = Tokenizer::new(&text);
while let Some(token) = tokenizer.next_token() {
println!("{token}");
}
}
Tokenizer<'a> is not a type that holds a string. It is a type that holds a
view of a string owned by somebody else, and the 'a propagates that fact into
the type system: a Tokenizer can never outlive the text it was built from, and
the compiler enforces it at every place a Tokenizer is stored or returned.
Notice what this buys. The tokenizer allocates nothing. Every token it hands back points into the original buffer, and a program that tokenizes a hundred-megabyte file does no copying at all. This is the design that lifetime parameters exist to make safe, and it is why Rust’s parsers, serializers and text tools are as fast as they are.
When a borrowing type is the wrong answer
That same power is the most common way for a newcomer’s design to go wrong, and the mistake is worth naming clearly because it costs people weeks.
The shape is this. You write a struct that owns its data. Somewhere a borrow
error appears. You notice that changing a String field to a &str field makes
the error go away, so you add a lifetime parameter. Now the struct that contains
that struct needs one too. Then the function returning it needs one, and the
trait implementation will not accept one, and eventually you are threading 'a
through eleven types to store a configuration value that is forty bytes long.
The diagnosis is that a lifetime parameter on a type is a statement that the type is a temporary view, and most types are not. Ask what the type is for. If it is created, used and dropped inside one function or one loop iteration — a parser over a buffer, an iterator, a formatter, a visitor walking a tree — then it is a view, and a lifetime is the right tool. If it is stored in a collection, returned up several layers, held across an await point, put in a struct that lives as long as the program, or sent to another thread, then it is storage, and it should own its data.
The cost of owning is an allocation and a copy. Compared to threading a lifetime parameter through a type hierarchy so that a config struct can avoid copying a path name once at startup, the allocation is free.
Coming from C++
The instinct that copying is expensive and must be avoided by default is correct in a language where a copy might be a deep copy of an object graph and a
const¶meter is how you opt out. Rust moves rather than copying, so passing by value is already cheap, andStringversus&stris a question about who owns the bytes rather than about how much work the call does. Reserve the borrowing design for the places where it removes real work — parsing, scanning, slicing large buffers — and let ordinary structs own what they hold.
The 'static misunderstanding
'static has two meanings that look identical and are not, and conflating them
produces some of the most confusing error messages in the language.
As the lifetime of a reference, &'static str means the referent lives for the
duration of the program. String literals are the everyday example: they are baked
into the binary, so let name: &'static str = "anderix"; is fine.
As a bound on a generic parameter, T: 'static means something weaker and much
more common: T contains no references with a lifetime shorter than the whole
program. Every owned type satisfies it. String is 'static. Vec<u64> is
'static. A struct of owned fields is 'static. What is not 'static is
Tokenizer<'a> from earlier, because it holds a borrow of something that might
go away.
So when std::thread::spawn requires its closure to be 'static, it is not
demanding that the closure run forever. It is saying that the closure may not
hold a reference to a local variable in the spawning function, because that
function may return while the thread is still running. Chapter 14 comes back to
this. For now, read T: 'static as “owns its data” and the errors stop being
mysterious.
Reading lifetime errors as design questions
Three messages account for most of what you will see, and each has a small set of sensible responses.
x does not live long enough means a reference is being kept past the life of
its referent. Either the referent should live longer — declare it in an outer
scope, or hoist it out of the loop — or the thing holding the reference should
own its data instead.
Cannot return reference to local variable means a function is trying to hand out a view of something it is about to drop. The function should return the owned value, or take a destination to write into.
Missing lifetime specifier means a signature is ambiguous about which input the output borrows from. Decide which one, and if the answer is “neither, it’s something I built in here”, the function should return an owned value.
In all three, the productive move is the one chapter 1 described: read the error as a question about the design. The failing shapes are genuinely broken programs, and the compiler is naming the break.
Conclusion
A lifetime is the region in which a reference is valid, and the compiler computes it for every reference in the program. Annotations describe relationships across a function boundary, where the compiler can see only the signature. Three elision rules cover the ordinary cases, so most code carries no annotations at all.
A lifetime parameter on a type turns that type into a view: it cannot outlive what it borrows from, which is precisely what you want for parsers and iterators and precisely what you do not want for anything the program stores. When a lifetime parameter starts spreading through your types, that is evidence you asked a storage type to be a view.
The next chapter turns from references to the types themselves, and to how a Rust type is assembled out of data, behavior and the invariants you want it to guarantee.
Questions to think through
The with_prefix example gives 'a to one parameter and not the other. Write
down what would break for a caller if both parameters shared 'a. Is anything in
the function body different?
A Tokenizer<'a> cannot be stored in a struct that outlives its input. Find a
type in a codebase you know that holds a pointer or reference to data it does not
own, and ask what currently prevents that data from being freed first.
T: 'static is satisfied by every owned type. Why is that the right bound for
thread::spawn, and what would go wrong if the bound were dropped?