Lifetimes: The Reference Validity Invariant
Lifetimes are the compiler's proof that every reference points to valid data. You don't control how long things live — you help the compiler verify that references never outlive what they point to.
Together with ownership and borrowing, lifetimes complete Rust’s memory safety model. Lifetimes are Rust’s most feared topic. People see 'a in a function signature and assume they need a PhD in type theory. They don’t. Lifetimes are a single idea: every reference must be valid for as long as it’s used. That’s it. The borrow checker enforces this, and lifetime annotations are how you help it when it can’t figure things out on its own.
The Problem: Dangling References
Consider an example. You have a function that returns a reference to a local variable:
1
2
3
4
5
6
7
8
9
fn dangling() -> &String {
let goodbye = String::from("see you never");
&goodbye
}
fn main() {
let reference = dangling();
println!("{}", reference);
}
The compiler rejects this immediately:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
error[E0106]: missing lifetime specifier
--> src/main.rs:1:19
|
1 | fn dangling() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value,
but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon
unless you're returning a borrowed value from a `const` or a `static`
|
1 | fn dangling() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
1 | fn dangling() -> String {
| ~~~~~~~
The compiler sees what you’re trying to do and says no. goodbye lives on the stack inside dangling. When the function returns, goodbye is dropped. If the compiler allowed you to return &goodbye, reference in main would point to freed memory. A dangling pointer. In C, this compiles silently and corrupts your program. In Rust, it never makes it past the compiler.
The invariant: no reference outlives the data it points to. Ever.
What Lifetimes Actually Are
Every reference in Rust has a lifetime — the region of code where that reference is valid. Most of the time, the compiler figures this out without your help:
1
2
3
4
5
fn main() {
let the_owner = String::from("owned data"); // the_owner's lifetime starts
let the_ref = &the_owner; // the_ref borrows the_owner
println!("{}", the_ref); // the_ref is used here
} // both go out of scope — no problem
the_ref doesn’t outlive the_owner. The compiler sees this and is satisfied. No annotations needed. Lifetimes are already there — they exist for every reference in every Rust program. You just don’t have to write them most of the time.
When You Do Need Annotations
The compiler needs help when a function takes references as input and returns a reference as output. It needs to know: how does the returned reference’s lifetime relate to the inputs?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn longer_greeting<'a>(morning: &'a str, evening: &'a str) -> &'a str {
if morning.len() > evening.len() {
morning
} else {
evening
}
}
fn main() {
let good_morning = String::from("Good morning, world!");
let result;
{
let good_evening = String::from("Evening!");
result = longer_greeting(&good_morning, &good_evening);
println!("The longer one: {}", result);
}
}
The 'a syntax is a lifetime annotation. It doesn’t change how long anything lives. It tells the compiler: “the returned reference will be valid for as long as both inputs are valid.” The compiler uses this to verify that the caller doesn’t use the returned reference after either input has been dropped.
Let’s try to break it:
1
2
3
4
5
6
7
8
9
fn main() {
let good_morning = String::from("Good morning, world!");
let result;
{
let good_evening = String::from("Evening!");
result = longer_greeting(&good_morning, &good_evening);
} // good_evening is dropped here
println!("{}", result); // ERROR: result might reference good_evening
}
1
2
3
4
5
6
7
8
9
10
11
error[E0597]: `good_evening` does not live long enough
--> src/main.rs:6:49
|
5 | let good_evening = String::from("Evening!");
| ------------ binding `good_evening` declared here
6 | result = longer_greeting(&good_morning, &good_evening);
| ^^^^^^^^^^^^^ borrowed value does not live long enough
7 | }
| - `good_evening` dropped here while still borrowed
8 | println!("{}", result);
| ------ borrow later used here
The compiler traced the lifetime through the function signature and caught the violation. result could hold a reference to good_evening, but good_evening dies before result is used. The invariant would break, so the code is rejected.
The Annotation Syntax
Lifetime parameters start with ' and are usually short lowercase names:
1
2
&'a str // a reference to a str with lifetime 'a
&'a mut String // a mutable reference to a String with lifetime 'a
In function signatures, you declare them in angle brackets and then use them on the references:
1
2
3
4
5
6
fn first_word<'a>(text: &'a str) -> &'a str {
match text.find(' ') {
Some(pos) => &text[..pos],
None => text,
}
}
This says: “the returned &str lives as long as the input text.” The compiler now knows that any caller must keep text alive for as long as they use the return value.
Lifetime Elision: When You Don’t Need to Write Them
Writing 'a on every function would be tedious. Rust has lifetime elision rules — patterns the compiler recognizes so you don’t have to annotate them:
- Each input reference gets its own lifetime.
fn foo(x: &str, y: &str)becomesfn foo<'a, 'b>(x: &'a str, y: &'b str). - If there’s exactly one input lifetime, it’s assigned to all output references.
fn foo(x: &str) -> &strbecomesfn foo<'a>(x: &'a str) -> &'a str. - If one of the inputs is
&selfor&mut self, the self lifetime is assigned to all output references.
These three rules cover most cases. That’s why you can write fn first_word(text: &str) -> &str without any annotations — rule 2 handles it. You only need explicit annotations when the compiler can’t determine the relationship between input and output lifetimes, which typically means multiple input references and a returned reference.
Lifetimes in Structs
If a struct holds a reference, it needs a lifetime annotation. The struct cannot outlive the data it references:
1
2
3
4
5
6
7
8
9
10
11
struct Excerpt<'a> {
text: &'a str,
}
fn main() {
let the_full_text = String::from("Call me Ishmael. Some years ago...");
let excerpt = Excerpt {
text: &the_full_text[..16],
};
println!("Excerpt: {}", excerpt.text);
}
The 'a on Excerpt means: “an instance of Excerpt cannot outlive the reference stored in its text field.” If the_full_text gets dropped while excerpt still exists, the compiler rejects it. The struct carries the invariant with it — wherever it goes, the compiler tracks that its reference must remain valid.
The ‘static Lifetime
One special lifetime: 'static. It means the reference is valid for the entire duration of the program.
1
let forever: &'static str = "I live forever";
String literals are 'static because they’re baked directly into the compiled binary. They don’t live on the heap or the stack — they’re part of the program itself.
You’ll also see 'static in trait bounds and error messages. When the compiler suggests adding 'static, pause and think about whether you actually need the data to live forever, or whether you should restructure to use owned data instead. Reaching for 'static to silence the compiler usually means you’re fighting the borrow checker instead of listening to it.
Lifetimes Are Not About Control
A common misconception: lifetime annotations control how long values live. They don’t. Values live as long as their scope dictates. Annotations are a communication tool — they tell the compiler how the lifetimes of different references relate to each other, so it can verify that no reference ever dangles.
Think of it this way: the borrow checker is trying to prove a theorem — “this reference is always valid.” Lifetime annotations are the hints you give it when the proof isn’t obvious from the code structure alone.
Lifetimes complete Rust’s reference safety story. Ownership determines when values are freed. Borrowing determines who can access them. Lifetimes prove that every reference is valid for as long as it’s used. Together, they make dangling pointers, use-after-free, and data races impossible at compile time — not through runtime checks, but through proof. Next up: collections, and how Vec, String, and HashMap each maintain their own invariants.
Stay in the loop
Subscribe via RSS to get new posts on systems, Rust, and cryptography.
Subscribe to RSS