17.2Lifetime annotations in functions
The first place you must write a lifetime by hand is a function that returns a reference borrowed from its inputs. This lesson introduces the 'a syntax on the classic longest function and, more importantly, makes clear what the annotation actually means, because the single most common misunderstanding is thinking lifetimes change how long things live. They don't. They describe.
A function that returns a reference
Here's a function that returns the longer of two string slices:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
This won't compile, and the reason is a real ambiguity. The function returns a reference, but which of its inputs does the returned reference borrow from? Sometimes x, sometimes y, decided at runtime by the lengths. The compiler needs to know the relationship between the inputs' lifetimes and the output's lifetime to guarantee the returned reference stays valid, and it can't infer it here:
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(x: &str, y: &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 `x` or `y`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
That help text is the clearest one-sentence statement of the whole problem: "the signature does not say whether it is borrowed from x or y." E0106 again, the same error as the struct case in lesson 10.8, and the suggested fix is exactly what we'll write.
The 'a syntax
A lifetime parameter looks like a generic type parameter (chapter 15) but starts with an apostrophe and is conventionally named 'a. You declare it in angle brackets after the function name, then attach it to the references:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let s2 = String::from("short");
println!("{}", longest(&s1, &s2));
}long string
Read <'a> as "for some lifetime 'a," and read the rest as a sentence: x: &'a str and y: &'a str both borrow for the same lifetime 'a, and -> &'a str returns a reference that also lasts for 'a. Together they tell the compiler: "the returned reference is valid for as long as both inputs are valid." That's the relationship the compiler needed. Now it can check at each call that the returned reference isn't used past the point where either input is still alive, exactly the dangling-reference protection from lesson 9.5, made expressible.
The crucial point: it describes, never extends
Here is the misunderstanding to kill now. Writing 'a does not make any value live longer. It is not a command; it's a description. The annotation tells the compiler about a relationship that already exists among the references, so the compiler can check it. It cannot create lifetime out of nothing.
You can prove this to yourself: if longest truly extended lifetimes, this would compile, but it doesn't:
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("short");
result = longest(&s1, &s2);
} // s2 dropped here
println!("{result}"); // refused
}error[E0597]: `s2` does not live long enough
--> src/main.rs:6:31
|
5 | let s2 = String::from("short");
| -- binding `s2` declared here
6 | result = longest(&s1, &s2);
| ^^^ borrowed value does not live long enough
7 | }
| - `s2` dropped here while still borrowed
8 | println!("{result}");
| ------ borrow later used here
Because longest says the output lives only as long as both inputs, and s2 dies at the inner brace, result can't be used afterward. The annotation didn't keep s2 alive; it described the constraint, and the constraint says result is only valid while both inputs are. Lifetimes are a way of stating the rules so the compiler can enforce them, never a way to bend them.
Key insight
A lifetime annotation is a promise about references, not a power over them. <'a>(x: &'a str, y: &'a str) -> &'a str promises "the output borrows from these inputs and is valid only as long as they are." The compiler verifies the promise holds; it never grants extra lifetime to satisfy it. If you ever think "I'll add 'a to make this live longer," stop, that's not what it does. The fix for "doesn't live long enough" is to restructure so the data genuinely lives long enough (or to own it), not to annotate harder.
When you don't need it
Not every reference-returning function needs an annotation. If a function returns a reference borrowed from exactly one input, the compiler infers the relationship (that's elision, lesson 17.4). longest needs the annotation precisely because it has two inputs and the output could come from either. A function like fn first_word(s: &str) -> &str (your friend from lesson 9.7) needs no annotation, because there's only one input the output could borrow from, so there's no ambiguity to resolve.
Quiz time
Question #1
Why does fn longest(x: &str, y: &str) -> &str fail to compile without a lifetime annotation?
Show solution
Because it returns a reference, but the signature doesn't say whether that reference borrows from x or y (it could be either, decided at runtime). The compiler needs to know the relationship between the inputs' and output's lifetimes to guarantee the returned reference stays valid, and it can't infer it with two candidate inputs. That's E0106, missing lifetime specifier.
Question #2
What does fn longest<'a>(x: &'a str, y: &'a str) -> &'a str tell the compiler?
Show solution
That both inputs and the output share the lifetime 'a: the returned reference is valid only for as long as both x and y are valid. This lets the compiler check at each call that the result isn't used after either input has gone out of scope.
Question #3
True or false: adding 'a to a function makes its arguments live longer. Explain.
Show solution
False. A lifetime annotation describes a relationship between references that already exists; it never extends how long any value lives. If the data doesn't actually live long enough, the program is still refused ("does not live long enough"). The fix is to restructure the code or use owned data, not to add more annotations.
Functions are the first place lifetimes surface. The second is the one lesson 10.8 explicitly deferred: structs that hold references. The next lesson pays that IOU and shows the <'a> on a struct definition.