Lifetimes
Lifetimes never make anything live longer — they're how you describe a relationship between references that the compiler already enforces.
So far, the borrow checker has quietly figured out how long every reference needs to live by looking at where it's created and where it's last used. That inference breaks down the moment a reference could plausibly come from more than one place. Consider a function that takes two string slices and returns whichever is longer: the return value is a reference, but a reference to what? The compiler cannot look inside the function body, see an if, and know in advance which branch will execute for a given call — so it can't work out on its own whether the returned reference should be tied to the first argument's lifetime or the second's.
This is the one situation where you have to help the compiler by writing a lifetime annotation — a name like 'a (read 'tick-a') that labels a lifetime so it can appear more than once in a signature. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str declares a generic lifetime parameter 'a, then reuses it on both parameters and the return type. This does not create a lifetime, extend one, or make anything live longer than it otherwise would — it's a constraint the function's signature promises to the caller: 'the reference I hand back is valid for exactly as long as the shorter-lived of the two references you gave me.'
The compiler then holds every caller to that promise. If you call longest with one string that goes out of scope earlier than the other, the compiler will refuse to let you use the result past that point — not because it re-analyzes the function body at every call site, but because the signature already told it the rule, and checking a caller against a rule is exactly what a borrow checker is built to do. This is also why the annotation buys you nothing if you get it wrong: writing 'a doesn't make the code correct, it makes a claim, and the compiler independently verifies the function body actually upholds that claim.
If every function needed explicit lifetime annotations, Rust would be unbearable to write, so the compiler applies three elision rules first and only asks you to spell things out when they don't resolve the ambiguity. First, every reference parameter gets its own implicit lifetime. Second, if there is exactly one input lifetime, it's assigned to every elided output lifetime. Third, if one of the parameters is &self or &mut self, its lifetime is assigned to every elided output — which is why methods that return a reference derived from self almost never need annotations. longest needed one only because it has two input references and no &self to break the tie.
Structs can hold references too, and when they do, the struct definition itself needs a lifetime parameter: struct Excerpt<'a> { part: &'a str } says that an Excerpt cannot outlive the string slice its part field borrows from. This is the compiler protecting you from a struct that would otherwise dangle — if the original String that part points into were dropped while an Excerpt referencing it was still around, you'd have a use-after-free, exactly the class of bug Rust exists to eliminate at compile time instead of at 3 a.m. in production.
The single most useful sentence to internalize about lifetimes is this: they don't change how long anything lives. A lifetime annotation is not a directive to the compiler to keep a value around longer, the way a garbage collector's reachability analysis might; it's a description of a relationship — 'this reference's validity depends on that one's' — that already exists in your code whether or not you write it down. Naming it just gives the borrow checker enough information to verify a relationship that spans a function boundary, where it otherwise can't see far enough to infer one on its own.
Because of that, fighting a lifetime error by sprinkling 'static everywhere or adding lifetime parameters until it compiles almost always makes things worse, not better — you're lying to the compiler about a relationship that doesn't actually hold, and it either won't compile anyway or will compile while quietly forcing values to live longer (and be cloned or leaked more) than they need to. When the borrow checker rejects a function, the fix is nearly always to restructure ownership — return an owned String instead of a borrowed &str, or restructure which value owns which — rather than to keep adding annotations until the compiler gives up arguing.
// Without a lifetime annotation, this wouldn't compile://// fn longest(x: &str, y: &str) -> &str {// if x.len() > y.len() { x } else { y }// }//// The compiler can't tell whether the returned reference is tied to// the lifetime of `x` or of `y`, so it refuses to guess.fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}}fn main() {let string1 = String::from("long string is long");let result;{let string2 = String::from("xyz");result = longest(string1.as_str(), string2.as_str());println!("The longest string is {}", result);}}
A function that returns a reference derived from one of two inputs needs an explicit lifetime because the compiler can't tell from the body alone which argument the result is tied to.
// Elided: the compiler fills in the lifetime using elision rule #2 —// exactly one input lifetime, so it's assigned to the output too.fn first_word(s: &str) -> &str {match s.find(' ') {Some(pos) => &s[..pos],None => s,}}// The exact same signature, written out by hand.fn first_word_explicit<'a>(s: &'a str) -> &'a str {match s.find(' ') {Some(pos) => &s[..pos],None => s,}}fn main() {let sentence = String::from("hello world");println!("{}", first_word(&sentence));println!("{}", first_word_explicit(&sentence));}
The same signature written twice: once relying on lifetime elision, once fully spelled out, to show exactly what the elision rules are filling in for you.
struct Excerpt<'a> {part: &'a str,}impl<'a> Excerpt<'a> {// Elision rule #3: a `&self` parameter lends its lifetime to the// return value, so no annotation is needed here even though this// method returns a reference.fn announce(&self, announcement: &str) -> &str {println!("Attention please: {}", announcement);self.part}}fn main() {let novel = String::from("Call me Ishmael. Some years ago...");let first_sentence = novel.split('.').next().expect("no '.' found");let excerpt = Excerpt {part: first_sentence,};println!("Excerpt: {}", excerpt.announce("New chapter"));}
A struct holding a borrowed reference must carry a lifetime parameter, and its method can still rely on elision because a &self parameter is present.
🧠 Check your understanding
0/1 · 0/1 answered1. What does adding the lifetime annotation 'a to fn longest<'a>(x: &'a str, y: &'a str) -> &'a str actually do?