The compiler rejects your reference
You write a function to compare two strings and return the longer one. The logic is trivial. You check the lengths and return one of the arguments. The compiler rejects the code with E0106 (missing lifetime specifier).
You stare at the screen. The function doesn't allocate memory. It doesn't mutate anything. It just passes a reference through. Why does Rust demand more information?
The compiler isn't being difficult. It's protecting you from a dangling pointer. When a function returns a reference, the compiler needs to know exactly which input that reference is tied to. If the input dies, the reference becomes a pointer to garbage. Rust refuses to guess. It forces you to write a contract that proves the reference stays valid.
What lifetime bounds actually do
Lifetimes are names for scopes. When you write a lifetime annotation, you're giving a scope a label. The compiler uses those labels to track how long references live relative to each other.
Think of a ticket stub for a ride at an amusement park. The ride closes at 10 PM. The ticket stub is valid until 10 PM. If the park shuts down the ride at 8 PM, the ticket stub is useless. The lifetime is the closing time. The reference is the ticket.
When a function returns a reference, it's handing out a ticket. The caller needs to know when that ticket expires. If the ticket is tied to input A, it expires when input A is gone. If it's tied to input B, it expires when input B is gone. The lifetime bound is the label on the ticket that says "This expires when the data in slot A expires."
Without the bound, the compiler can't verify the ticket is valid. It assumes the worst and rejects the code.
The minimal fix
The fix is to add a lifetime parameter to the function signature and attach it to the references.
/// Returns the longer of two string slices.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// Compare lengths to decide which reference to return.
if x.len() > y.len() {
x
} else {
y
}
}
The <'a> after the function name introduces a lifetime parameter named 'a. This is just a label. You could name it 'x or 'life, but the convention is to use 'a for the first lifetime.
The &'a str syntax ties the reference to that label. It means "a string slice that lives at least as long as 'a". By putting &'a str on both inputs and the output, you're telling the compiler: "The return value is tied to the same scope as both inputs. As long as both inputs are valid, the output is valid."
The compiler checks this contract. If you try to use the returned reference after one of the inputs goes out of scope, the compiler rejects the code. The lifetime bound makes the relationship explicit.
Tie the output to the input. The compiler checks the math.
Why the compiler needs your help
Rust has a feature called lifetime elision. The compiler can infer lifetimes automatically in simple cases. This is why fn trim(s: &str) -> &str compiles without annotations.
The elision rule is straightforward: if a function has exactly one input reference, the compiler assumes the output reference lives as long as that input. There's only one choice, so the compiler makes it.
When you have multiple input references, the compiler stops guessing. It won't assume the output is tied to x or y or both. It demands you specify. This is a safety feature. If the compiler guessed wrong, you'd get a dangling pointer at runtime. By forcing you to write the annotation, Rust ensures the contract matches your intent.
You might write a function that returns a reference to x but accidentally annotate it as tied to y. The compiler will catch this at the call site. If you pass a short-lived y and a long-lived x, the compiler sees the mismatch and rejects the code. The annotation protects you from your own mistakes.
Don't fight the compiler here. Write the annotation that matches the logic.
Structs and the lease analogy
Lifetimes appear in structs when the struct holds references. A struct with a reference is like a lease on an apartment. The struct exists, but it depends on the apartment existing too. If the apartment is demolished, the lease is void.
/// Holds a reference to a part of text.
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
/// Returns a level indicator.
fn level(&self) -> i32 {
3
}
}
The <'a> on the struct definition introduces the lifetime parameter. The field part: &'a str ties the reference to that lifetime. This means the ImportantExcerpt struct cannot outlive the string slice it references.
The impl block also needs <'a>. This scopes the methods to a specific lifetime. Even if a method doesn't use the lifetime, the impl block must declare it to match the struct.
Convention aside: Rustaceans almost always name the first lifetime 'a. If you have a second independent lifetime, use 'b. Descriptive names like 'data are rare and usually signal that the lifetimes are getting too tangled. Stick to 'a and 'b until you have a reason to break the pattern.
Structs with references are leases. The lease expires when the data vanishes.
Pitfalls that trip up beginners
Lifetime errors often come from over-constraining or misunderstanding what lifetimes can fix.
Over-constraining lifetimes
You might tie lifetimes together when the logic doesn't require it. Consider a function that returns the first word of a string, ignoring a second argument.
// BAD: Forces both inputs to share the same lifetime.
fn first_word<'a>(x: &'a str, _y: &'a str) -> &'a str {
x.split_whitespace().next().unwrap_or("")
}
This signature says the return value is tied to both x and _y. The caller must keep _y alive as long as x, even though _y is never used. This breaks valid code where _y is short-lived.
The fix is to give _y a distinct lifetime.
// GOOD: Only x constrains the output lifetime.
fn first_word<'a>(x: &'a str, _y: &str) -> &'a str {
x.split_whitespace().next().unwrap_or("")
}
Now _y can be a short-lived reference. The compiler sees the return value is only tied to x. The API is flexible and correct.
Don't force lifetimes to match if the logic doesn't require it. Over-constraining breaks your API.
Returning a reference to a local variable
Lifetimes can't save you if you try to return a reference to data created inside the function.
// ERROR: E0515 cannot return value referencing local variable
fn make_ref() -> &str {
let s = String::from("hello");
&s
}
The variable s is created inside the function. It gets dropped when the function returns. The reference &s points to memory that is about to be freed. No lifetime annotation can fix this. The compiler rejects the code with E0515.
The fix is to return an owned value instead of a reference.
/// Returns a new string.
fn make_owned() -> String {
String::from("hello")
}
Owned values move out of the function. The caller takes responsibility for the data. Lifetimes are for sharing references, not for extending the life of local variables.
Lifetimes prove references are safe. They can't resurrect dead data.
When to use lifetimes versus owned types
Choosing between lifetimes and owned types depends on ownership and performance.
Use lifetime annotations on functions when the return type is a reference derived from an input argument. Use lifetime annotations on structs when the struct contains references to external data. Use the 'static lifetime when the data lives for the entire duration of the program, like string literals or global constants. Reach for owned types like String or Vec when the data needs to outlive the input references or when multiple owners are required. Skip lifetime annotations when the function returns an owned value or takes no references, as the compiler infers everything automatically.
Lifetimes are a tool for sharing, not a punishment. Use them to prove safety, not to fight the type system.