Lifetime Annotations in Functions
A lifetime annotation is a piece of syntax, written as an apostrophe followed by a short name like 'a, that tells the Rust compiler how the lifetimes of several references relate to one another. It does not change how long any value actually lives — it is purely a label that lets the borrow checker verify, at compile time, that a function never returns or stores a reference that could outlive the data it points to. Once you understand what problem lifetime annotations solve, the syntax itself is small; the hard part is building the right mental model, which this lesson focuses on before touching any code.
Overview: How Lifetimes Work
Every value in Rust has an owner, and when that owner goes out of scope the value is dropped and its memory is freed. A reference (&T or &mut T) lets you borrow a value without taking ownership of it, but a reference is only valid for as long as the data it points to is still alive. The span of the program during which a particular reference is guaranteed to point at valid memory is called that reference’s lifetime. Lifetimes exist for every reference whether or not you write them down — most of the time the compiler works them out for you through a set of rules called lifetime elision (covered below). You only need to annotate lifetimes explicitly when the compiler cannot work out, on its own, how the lifetimes of several references relate to each other.
Think of a lifetime annotation as a coat-check ticket rather than the coat itself. Writing 'a on a reference doesn’t hand out any extra time for the underlying data — it just labels that reference so the compiler can match it up with other references carrying the same label and confirm they’re all talking about data that lives at least as long as required. If a function takes two references and its signature says they share the label 'a, the compiler treats 'a as the smaller of the two actual lifetimes at each call site — the guarantee is only as strong as the shorter-lived input.
Consider a function that takes two string slices and returns the longer one. Inside the function body, the compiler can see that the return value is either x or y — but at compile time it has no idea which branch will run for a given call, and it has no idea how long the caller’s x and y will actually live relative to each other. Without more information it must reject the function, because if it let the reference through and the caller later used the result after the shorter-lived input was dropped, that would be a dangling reference — a read of freed memory, exactly the class of bug Rust’s ownership system exists to eliminate at compile time instead of at runtime with a garbage collector, or worse, not at all. A lifetime annotation is how you supply the missing information: you tell the compiler "the reference I return lives at least as long as the shorter of x and y," and the compiler then checks every call site against that promise.
In many cases you never write a lifetime annotation because the compiler infers it using three lifetime elision rules, applied in order:
| Rule | What the compiler does |
|---|---|
| Rule 1 | Each elided reference parameter gets its own distinct lifetime parameter. |
| Rule 2 | If there is exactly one input lifetime parameter, that lifetime is assigned to all elided output lifetimes. |
| Rule 3 | If one of the parameters is &self or &mut self, the lifetime of self is assigned to all elided output lifetimes. |
If, after applying all three rules, the compiler still cannot assign a lifetime to every reference in the signature, it stops and asks you to annotate explicitly — that’s the E0106 "missing lifetime specifier" error you’ll see in the Common Mistakes section below. A function taking a single &str parameter and returning a &str needs no annotation (rule 2 covers it); a function taking two independent &str parameters and returning a &str does need one, because the compiler cannot tell which parameter the return value’s lifetime should be tied to.
Syntax
The general form of a lifetime-annotated function signature looks like this:
fn function_name<'a>(param1: &'a str, param2: &str) -> &'a str {
// body uses param1 and/or param2
}
<'a>right after the function name declares a generic lifetime parameter, the same way<T>declares a generic type parameter.'ais just a name; convention uses short lowercase names starting from'a,'b,'c, but any valid identifier works.- Each
&'a Typeparameter says "this reference must live at least as long as ‘a". - A
&'a Typereturn type says "the reference I’m handing back is only valid as long as ‘a holds" — it ties the output’s validity to at least one of the inputs. - Lifetime parameters can be combined with type parameters and trait bounds in the same angle brackets, e.g.
fn f<'a, T>(x: &'a T).
Examples
The classic motivating example is a function that returns the longer of two string slices, where both inputs and the output share a single lifetime parameter:
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 string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
Output:
The longest string is long string is long
string1 and string2 are both owned Strings; .as_str() borrows each as a &str for the call. Because the signature ties x, y, and the return type to the same 'a, the compiler enforces that result cannot be used past the point where either string1 or string2 would normally go out of scope. Here both live for the whole of main, so printing result is fine.
Not every lifetime parameter has to affect the return type. A function can take references with different lifetimes and only tie the one that matters to the output:
fn first_word<'a, 'b>(text: &'a str, _prefix: &'b str) -> &'a str {
text.split_whitespace().next().unwrap_or(text)
}
fn main() {
let sentence = String::from("lifetimes are not that scary");
let label = String::from("note:");
let word = first_word(sentence.as_str(), label.as_str());
println!("First word: {}", word);
}
Output:
First word: lifetimes
text and _prefix borrow independently — 'a and 'b are unrelated. Because the return type is &'a str, only the caller’s use of sentence constrains how long word can live; label (bound to 'b) could be dropped immediately after the call and the compiler would still accept the code, because the return value was never tied to 'b in the first place.
Lifetime parameters aren’t limited to free functions — a struct that stores a reference must declare a lifetime parameter too, because the compiler needs to know the reference inside the struct can’t outlive the data it borrows:
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
fn announce_and_return_part(&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("could not find a '.'");
let excerpt = Excerpt { part: first_sentence };
println!("Excerpt: {}", excerpt.announce_and_return_part("New chapter"));
}
Output:
Attention please: New chapter
Excerpt: Call me Ishmael
Excerpt<'a> can only exist as long as the &'a str it holds in part is valid — the struct’s lifetime parameter is threaded through the impl block with impl<'a> Excerpt<'a>. Inside announce_and_return_part, the &str return type has no explicit lifetime, but elision rule 3 applies: because one of the parameters is &self, the compiler assigns self’s lifetime to the elided return type, which is compatible with returning self.part.
How It Works Step by Step
Walking through the call to longest from the first example:
- The compiler sees
fn longest<'a>(x: &'a str, y: &'a str) -> &'a strand treats'aas a placeholder to be filled in separately at every call site. - At the call
longest(string1.as_str(), string2.as_str()), it looks at how long the borrows ofstring1andstring2are each valid for. - Because the signature requires both parameters to satisfy the same
'a, the compiler picks'ato be the intersection (the shorter) of those two borrow spans — the annotation doesn’t force the two inputs to live equally long, it just requires the shared guarantee to hold for as long as both are needed. - The return type
&'a stris then only guaranteed valid for that intersection. Whenresultis used later inprintln!, the compiler checks that this use happens before either input would go out of scope — since both live until the end ofmain, the check passes. - If
string2had instead been created inside a shorter-lived inner block, andresultwere used after that block ended, the borrow checker would reject the program, because the use ofresultwould extend past the shorter of the two input lifetimes that'awas inferred to be.
Common Mistakes
It’s tempting to write a two-reference-in, one-reference-out function and expect the compiler to figure out lifetimes the way it does for a single-input function. It can’t:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
This fails to compile with error[E0106]: missing lifetime specifier. Elision rule 2 only applies when there is exactly one input lifetime; with two independent &str parameters, the compiler has no way to know whether the returned reference’s validity should be tied to x, to y, or to both, so it refuses to guess. The fix is the explicit annotation from the first example above: giving x, y, and the return type the same 'a tells the compiler the return value is valid only as long as both inputs are, which is exactly the guarantee the function actually provides.
Lifetime annotations can describe relationships between existing references, but they cannot invent a longer lifetime for data that doesn’t have one. Trying to return a reference to a value created inside the function is a common mistake for newcomers coming from garbage-collected languages:
fn dangle() -> &String {
let s = String::from("hello");
&s
}
s is created inside dangle and dropped the moment the function returns, so &s would point at freed memory — precisely the dangling-pointer bug lifetimes exist to prevent. No annotation fixes this, because there is no lifetime in the caller that the returned reference could correctly borrow from; the compiler rejects the function outright (first with a missing lifetime specifier error, and it would still reject it as a local-value escape even if you added an explicit annotation). The real fix is to stop borrowing and return owned data instead:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
fn main() {
let result = no_dangle();
println!("{}", result);
}
Output:
hello
Returning the owned String moves it out to the caller instead of borrowing, so there’s no reference — and no lifetime problem — at all.
Best Practices
- Let elision do the work: don’t write
<'a>on a function that only has one input reference, or where the references genuinely don’t need to be related — the compiler will tell you exactly when an explicit annotation becomes necessary viaE0106. - Prefer owned types like
StringandVec<T>over references in struct fields and return types until you have a concrete, measured reason to borrow; a reference stored in a struct spreads a lifetime parameter into every type that holds it, which multiplies complexity. - When a struct holds a single reference, the idiomatic name for its lifetime parameter is just
'a; reach for'a,'b,'conly when a type genuinely needs to track more than one independent lifetime. - Reach for
'staticonly when a reference genuinely lives for the whole program (string literals, or data intentionally leaked withBox::leak) — it is not a generic fix for lifetime errors, and overusing it tends to just move the error to a worse place. - Read compiler lifetime errors carefully; recent versions of
rustcusually suggest the exact annotation to add, and applying that suggestion is a legitimate way to learn the pattern. - If a function’s lifetime signature is getting hard to reason about, that’s often a sign it’s borrowing more than it needs — consider whether it could take ownership instead, or return an owned value rather than a reference.
Practice Exercises
- Write a function
fn shortest<'a>(x: &'a str, y: &'a str) -> &'a strthat returns the shorter of two string slices, and call it frommainwith two different string literals. Print the result. - Without running the compiler, decide whether this function needs an explicit lifetime annotation, then check your answer by trying to compile it:
fn first_char(s: &str) -> Option<char>(hint: apply the three elision rules by hand). - Define a struct
Pair<'a>holding two&'a strfields, and a method on it that returns whichever field is alphabetically first. Think about which elision rule, if any, lets the method’s return type skip an explicit lifetime.
Summary
- A lifetime annotation names the relationship between the lifetimes of references in a signature; it never extends how long a value actually lives.
- Most functions need no annotation at all because of the three lifetime elision rules; annotations are only required when the compiler can’t infer a unique relationship on its own (error
E0106). - Tying two parameters and a return type to the same
'ameans the return value is only guaranteed valid for the shorter of the two inputs’ lifetimes. - A struct that stores a reference must declare a lifetime parameter, and every
implblock for that struct threads the same parameter through. - No annotation can fix a genuinely dangling reference (returning
&sfor a locals) — that requires returning owned data instead. 'staticis a special lifetime for data that lives for the whole program; use it deliberately, not as a generic fix for lifetime errors.
