Why this matters
In module 5 you learned that a reference must never outlive the value it points to, and that the compiler works this out within a function. At function boundaries and inside structs it sometimes needs your help: when a function returns a reference, which input does it point into? When a struct holds a reference, how long must the referenced data live? Lifetime annotations answer those questions. They are the most feared part of Rust syntax, but the idea is small: they name relationships between borrows so the compiler can check them. They never change how long anything lives.
The problem
fn longest(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
println!("{}", longest("hello", "hi"));
}
error[E0106]: missing lifetime specifier
--> main.rs:1:33
|
1 | fn longest(a: &str, b: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
| ++++ ++ ++ ++
The function returns a reference into either a or b, depending on run-time data. A caller needs to know how long the result stays valid, and the borrow checker needs to check the caller without reading the function body (it checks each function on its own, against its signature). So the signature must state the relationship.
Lifetime parameters
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let s1 = String::from("a long string");
let result;
{
let s2 = String::from("short");
result = longest(&s1, &s2);
println!("inside: {result}");
}
}
inside: a long string
'a(an apostrophe and a name, usually short) is a lifetime parameter, declared in<>like a type parameter.&'a strmeans "a string slice valid for at least the lifetime'a".- The signature says: both inputs live at least as long as
'a, and the result is valid for'a. In practice, the compiler picks'aas the overlap of the two inputs' lifetimes, the shorter one.
So at the call site, the result may be used only while both arguments are still valid:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let s1 = String::from("a long string");
let result;
{
let s2 = String::from("short");
result = longest(&s1, &s2);
}
println!("outside: {result}");
}
error[E0597]: `s2` does not live long enough
--> main.rs:10:31
|
9 | let s2 = String::from("short");
| -- binding `s2` declared here
10 | result = longest(&s1, &s2);
| ^^^ borrowed value does not live long enough
11 | }
| - `s2` dropped here while still borrowed
12 | println!("outside: {result}");
| -------- borrow later used here
You can see that result actually points at s1, which is still alive. But the signature only promises "valid while both are", and the compiler checks callers against the signature, not the body. That is a feature: the function could change to return b tomorrow without breaking any caller's safety.
Annotations describe, they do not prescribe
A lifetime annotation never extends or shortens how long a value lives. Values live until their owner's scope ends, as always. Annotations only relate lifetimes so the compiler can verify that the code is consistent. If the body returned a reference to a local, no annotation could make it valid (module 5's E0515).
Not every parameter needs the same lifetime. If the result only comes from the first input, say so:
fn before_colon<'a>(text: &'a str, _sep_hint: &str) -> &'a str {
match text.find(':') {
Some(i) => &text[..i],
None => text,
}
}
fn main() {
let line = String::from("key: value");
let k;
{
let hint = String::from(":");
k = before_colon(&line, &hint);
}
println!("{k}");
}
key
hint may die early because the result is tied only to text.
Elision: why you rarely write them
Most functions with references need no annotations, because the compiler applies three elision rules:
- Each reference parameter gets its own lifetime.
- If there is exactly one input lifetime, it is assigned to all output references.
- If there is a
&selfor&mut selfparameter, its lifetime is assigned to all output references.
So fn first_word(s: &str) -> &str is understood as fn first_word<'a>(s: &'a str) -> &'a str, and a method fn name(&self) -> &str returns something borrowed from self. You write annotations only when the rules do not determine the answer: typically two or more reference inputs and a reference output, no self.
Structs that hold references
A struct can store a reference, but then the struct must not outlive the data it borrows. The lifetime becomes a parameter of the type:
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn first_sentence(&self) -> &'a str {
self.text.split('.').next().unwrap_or("")
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let e = Excerpt { text: &novel };
println!("{}", e.first_sentence());
}
Call me Ishmael
Excerpt<'a> reads "an Excerpt that borrows something for 'a". The compiler ensures no Excerpt outlives novel. Such structs are views: cheap to create, no copying, ideal for parsers and iterators over borrowed data. When a struct should be independent of where its data came from, store owned data (String) instead, which is why owned fields are the default advice.
'static
'static is the lifetime of the whole program. String literals are &'static str, since they are stored in the executable. A bound T: 'static means "T contains no non-static references" (it owns all its data), which you will meet with threads in module 14, since a spawned thread may outlive the function that started it.
Resist the temptation to fix lifetime errors by writing 'static everywhere: it demands that the data live forever, which local data cannot.
Common mistakes
- E0106 missing lifetime specifier on a function with several reference inputs: add a parameter and decide which inputs the output is tied to.
- E0597 / "borrowed value does not live long enough" at a call site: the signature ties the result to an input that dies too early. Either restructure the caller or loosen the signature (tie the output only to the inputs it really borrows from).
- Using references in structs by default: it spreads lifetime parameters through every type that contains them. Prefer owned data until you have a reason to borrow.
- Forgetting
impl<'a>: methods onExcerpt<'a>needimpl<'a> Excerpt<'a>.
Recap
- A lifetime parameter
'anames how long a borrow is valid;&'a Tis a reference valid for'a. - In
fn f<'a>(x: &'a T, y: &'a T) -> &'a T, the result is valid while both inputs are. - Annotations describe relationships; they never change how long values live.
- Elision rules cover most functions: one input, or
&self, determines the output lifetime. - Structs holding references need lifetime parameters and cannot outlive the borrowed data.
'staticmeans "for the whole program": literals, and types that own all their data.
Going further: the Rust Book chapter 10.3, "Validating References with Lifetimes", walks through the same ideas with diagrams. Try writing fn split_pair<'a>(s: &'a str) -> (&'a str, &'a str) and calling it on a String.