Why `fn longest(x: &str, y: &str) -> &str` Does Not Compile
You write the smallest function that seems to need no explanation:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
and the compiler answers with an error you will see many times in interviews and in real code:
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:33
|
1 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
Nothing in the body is wrong: both x and y are &str, and returning either one is perfectly legal at run time. What is missing is a relationship. The return value is a reference, but the signature never says where it borrows from. The bare type &str in return position could, in principle, be a borrow of x, a borrow of y, or a reference to a string literal that lives forever. The compiler refuses to guess, because each guess permits different caller code.
This is where lifetime syntax enters. A lifetime is the span during which a reference is valid: from where the reference is created to its last use. Borrow checking already reasons about those spans implicitly for local variables — you met that machinery in Refactoring Borrow Checker Errors. Named annotations do not create lifetimes or make anything live longer. They name the relationship between the lifetimes of the references in a signature so the compiler can check the claim.
The corrected signature is:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Read it aloud with precision: for some lifetime 'a, both inputs are borrowed for at least 'a, and the output is borrowed for exactly 'a. The angle brackets after the function name introduce 'a as a parameter of the function — a name the caller and the body share.
Here are the three signatures a programmer might reach for, and what each one promises:
| Signature | What it promises | Outcome |
|---|---|---|
-> &str |
the result is some borrow of unknown origin | rejected: E0106, origin ambiguous between two inputs |
-> &'static str |
the result lives for the whole program | rejected: neither x nor y is 'static, and the body may return either |
<'a>(x: &'a str, y: &'a str) -> &'a str |
the result follows the inputs' shared span | compiles: the caller instantiates 'a |
Modern rustc words the diagnostic as expected lifetime parameter with a help: consider introducing a named lifetime parameter note that even suggests the exact edit. Take the suggestion, then check it against the table — the suggested annotation is only correct if the body actually returns data from the annotated inputs.
Note where the error appeared: on the return type, not on the parameters. Input references without annotations are fine in this position; it is the ambiguous output that triggers E0106. That asymmetry is not random — it is the seed of the elision rules you will read in Lifetime Elision Rules, which exist precisely to spare you writing <'a> on signatures like this one. It also explains why fn first_word(&self) -> &str compiles with no annotation at all: when a receiver is present, there is a default answer for the output, so no diagnostic fires. Learn to read the error's caret placement: it points at the reference the compiler cannot connect to any input.
Reading `<'a>` as a Contract the Caller Must Satisfy
A lifetime parameter is not a duration you choose. You cannot write <'a> and decide it means "long enough for my loop". The annotation is a relation between references, and the actual value of 'a is chosen at each call site by the caller — the compiler unifies it with the lifetimes of the arguments the caller passes.
Consider what happens at a call site:
let name;
let result;
{
let short = String::from("rust");
name = String::from("ownership");
result = longest(short.as_str(), name.as_str());
} // short dropped here
println!("{}", result);
short dies at the end of the inner block while name lives on. The signature promises the result is borrowed for 'a, so 'a can be no longer than the shorter of the two input borrows — here, the borrow of short. Since result is used after short is dropped, the borrow check rejects the program: borrowed value does not live long enough. The annotation did not lie; you asked for a value that outlived its guarantee.
This is the practical reading of the "long enough" rule: the output borrows at least as long as the shorter input, and never longer. In the common case where both inputs live in the same scope, 'a collapses to that scope and everything compiles without drama. The annotation only becomes visible when the two borrows have different ends.
Three details interviewers probe:
- One parameter can serve many references.
x: &'a str, y: &'a strforces both inputs to share one lifetime. If you instead write<'a, 'b>withx: &'a str, y: &'b str, the return type must pick one —-> &'a strsays the result followsx's borrow, and the compiler then requiresy's borrow to be irrelevant to the result. - Structs, impls, and methods take the same syntax.
impl<'a> Reader<'a> { ... }means "an impl for the reader borrowing for'a". The angle bracket is the same token everywhere; only where it is declared changes. - The name is arbitrary.
'a,'x,'out,'inputall compile. Descriptive names such as'inputmake signatures self-documenting, and'static(covered in The static Lifetime) is the one name with a fixed meaning.
Bounds can attach too: <'a, 'b: 'a> reads "'b outlives 'a", which you will use when a struct holds two references of different spans.
The same parameter appears wherever a borrow crosses an API boundary:
struct Pair<'a> { left: &'a str, right: &'a str }
impl<'a> Pair<'a> {
fn longer(&self) -> &'a str {
if self.left.len() >= self.right.len() { self.left } else { self.right }
}
}
fn build<'a>(s: &'a str) -> Pair<'a> {
Pair { left: s, right: "literal" } // &'static str coerces down to any 'a
}
Notice build: the literal borrow is 'static, and a 'static reference satisfies any 'a the caller picks, so one parameter covers both fields. Mixed spans only become trouble when the output could follow the shorter-lived side — then you either unify everything under one 'a or split into 'a and 'b and commit to one of them.
A useful drill for interviews: for any annotated signature, ask who enforces each half? The caller enforces the input claim by handing over references that really live for 'a; the body enforces the output claim by producing a reference that lives that long. An annotation that satisfies neither side is a wish, and the compiler reports which party failed: a body error names a value that does not live long enough, while a call-site error names an argument whose borrow ends too soon.
What Annotations Guarantee — and the Zero Cost of Getting Them Wrong
The single most common misconception about lifetime annotations is that they affect the running program. They do not. Lifetimes exist only during compilation; rustc erases them before code generation, and a function with three lifetime parameters produces exactly the same machine code as one with none. There is no run-time bookkeeping, no reference counting, no extra register. The annotation's entire effect is on which programs compile.
That gives you a clean way to reason about errors. If you write an annotation that overstates reality, you never get a run-time failure — you get a compile error, and the wording tells you which claim failed:
error: lifetime may not live long enough
--> src/lib.rs:4:35
|
4 | let outer: &'a str = inner.as_str();
| ^^^^^^^^^^^^^^ borrowed value does not live long enough
The two directions of failure look like this:
// Understated: you claimed the output only borrows for 'a,
// but the body returns something that dies sooner.
fn too_short<'a>(x: &'a str, v: &String) -> &'a str {
v.as_str() // error: `v` does not live long enough
}
// Overstated: you required both inputs to share 'a, but the
// caller passes a short-lived borrow and a 'static one.
fn requires_unified<'a>(x: &'a str, y: &'a str) -> &'a str { x }
let leaked: &'static str = "literal";
{
let owned = String::from("temp");
requires_unified(leaked, &owned); // compiles: 'a shrinks to `owned`'s span
} // then `leaked` is still fine afterwards
The second example shows the key asymmetry: widening a lifetime in a parameter is always safe because the caller simply chooses a smaller 'a. Trouble only starts when the output promises more than an input can deliver.
A compact triage table for the diagnostics you will actually see:
| Error text | What it means | The fix |
|---|---|---|
missing lifetime specifier (E0106) |
output reference has no origin | add <'a> and tie the return type to an input |
lifetime may not live long enough |
body returns data shorter-lived than promised | annotate the source parameter too, or return the other data |
borrowed value does not live long enough at a call site |
result used after the shortest argument died | restructure scopes, clone, or hold the owner longer |
explicit lifetime required when returning Box<dyn Trait> |
the trait object's default lifetime is too weak | write Box<dyn Trait + 'a> to state the borrow |
Each message names the exact reference at fault, which is why reading the whole diagnostic — not just the first line — is faster than guessing.
Two habits keep you out of trouble. First, annotate only what the compiler cannot infer — if you must add <'a> to make the signature unambiguous, do it and stop; adding redundant parameters elsewhere just adds names to keep straight. Second, when an error mentions 'a, do not respond by sprinkling 'static to silence it. Promising 'static where you mean "a local borrow" pushes the problem onto every caller and often converts a small fix into a design change.
In code review and on a whiteboard, the expected answer to "what does <'a> do?" is one sentence: it lets the function state how the lifetimes of its reference arguments and its return value relate, so the borrow checker can verify the result is never used after the data it points at is gone. Everything above is the elaboration of that sentence.
Quiz
1. What exactly does the compiler reject in `fn longest(x: &str, y: &str) -> &str`?
2. A function is declared `fn longest<'a>(x: &'a str, y: &'a str) -> &'a str`. What does the caller decide at each call site?
3. Which statement about lifetime annotations is true?
4. You write `fn borrow<'a>(x: &'a str, v: &String) -> &'a str { v.as_str() }` and get an error. What is wrong?
Flashcards
Question
What is the precise meaning of `fn longest<'a>(x: &'a str, y: &'a str) -> &'a str`?
Click to reveal answer
Answer
There exists a lifetime `'a` such that both arguments are borrowed for at least `'a` and the returned reference is borrowed for `'a`. The caller instantiates `'a` from the actual argument borrows, so the result is valid no longer than the shorter of the two input borrows.
Question
Do lifetime annotations change how long a value lives or the generated code?
Click to reveal answer
Answer
No. They only state relationships between existing borrows so the compiler can check them. Lifetimes are erased before code generation, so annotated and unannotated versions of a valid program compile to identical machine code and nothing runs slower.
Question
Which error says you omitted a required lifetime annotation, and where does the caret usually point?
Click to reveal answer
Answer
`error[E0106]: missing lifetime specifier` (older wording: `expected lifetime parameter`). The caret points at the reference the compiler cannot relate to any input — almost always the return type when a function has multiple reference parameters and no `self` receiver.
Question
Your annotated function compiles, but a caller fails with `borrowed value does not live long enough`. What does that mean?
Click to reveal answer
Answer
The caller is trying to use the returned reference after the shortest input borrow has ended. The annotation was accurate; the surrounding code kept a result that outlives the data it points at, so the borrow must be moved, cloned, or the data's scope extended.
Question
When do you need two lifetime parameters instead of one on a function?
Click to reveal answer
Answer
When the inputs genuinely have independent spans and the output must follow only one of them — for example `fn pick<'a, 'b>(x: &'a str, y: &'b str) -> &'a str`, which says the result tracks `x` regardless of how long `y` lives. With a single `'a` you force both borrows to unify to the shorter one.
Revision Notes
Key Takeaways
- 1. E0106 fires on the return type when multiple input lifetimes leave the output's origin ambiguous — the compiler refuses to guess which input you are returning
- 2. `'a` is instantiated by the caller from the argument borrows; the result is valid for at most the shorter of the two input spans
- 3. Annotations are compile-time only: they are erased before code generation and never extend a value's life
- 4. Two parameters (`<'a, 'b>`) let the output follow one input while the other keeps an independent span
- 5. Overstating a lifetime on the output breaks callers; understating it breaks the body — the error message tells you which side failed
Interview Tips
- • "Why doesn't `fn longest(x: &str, y: &str) -> &str` compile?" — answer that the return reference could come from either input and elision cannot disambiguate two inputs without a receiver, so the compiler demands an explicit relationship
- • "What does `'a` actually do at run time?" — say plainly: nothing; it is a compile-time relation erased during codegen, which is why Rust has zero-cost borrow checking
- • "Can you make a value live longer by annotating it?" — no; you can only state a relationship that already holds. Extending life requires changing ownership (clone, move, or a longer-lived owner)
- • "Two references in, one out — how do you decide the annotation?" — derive it from the body: if you return one parameter's data, tie the output to that parameter's lifetime; only unify both when you might return either
Cheat Sheet
Lifetime Annotations
Read a signature out loud:
| Signature | Spoken meaning |
|---|---|
fn f<'a>(x: &'a str) -> &'a str |
output borrows from x, exactly as long |
fn f<'a>(x: &'a str, y: &'a str) -> &'a str |
both inputs unify to one span; output follows it |
fn f<'a, 'b>(x: &'a str, y: &'b str) -> &'a str |
output follows x; y is independent |
impl<'a> S<'a> { ... } |
methods on a struct that stores a borrow |
Rules of thumb
- Annotation = relationship, not duration. Nothing lives longer because you named it.
- Output lifetime can never exceed the shorter input lifetime.
- Annotate only ambiguous signatures; elision handles the rest (Lifetime Elision Rules).
- Adding
'ato a parameter is caller-friendly (call site shrinks it); over-promising on the output breaks callers. - Never reach for
'staticjust to silence a borrow error — see The static Lifetime.
Error triage:
E0106 missing lifetime specifier -> output not tied to any input; add <'a>
lifetime may not live long enough -> output promise exceeds an input's span
borrowed value does not live long enough (at a call site) -> result outlives shortest arg
Minimal template:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}