The compiler needs a contract, not a guess
You write a function that takes two string slices and returns the longer one. The logic is trivial. You compare lengths, pick the winner, and return it. The compiler rejects you with E0106 (missing lifetime specifier). You stare at the screen. The return value clearly comes from one of the inputs. Why does Rust refuse to compile this?
The compiler isn't being difficult. It's protecting you from a dangling reference that your brain just glossed over. When you return a reference, the compiler needs to know exactly how long that reference stays valid. It ties the output's lifetime to the inputs' lifetimes. Without that tie, the compiler assumes the worst. It assumes the reference might outlive the data. It rejects the code.
References are tickets, not owners
A reference is a view into memory owned by something else. You don't own the data. You just look at it. The danger is the owner drops the data while you're still looking. That's a dangling reference. Accessing a dangling reference is undefined behavior. Rust prevents this by tracking lifetimes.
Think of a reference as a ticket to a ride. The ride has a schedule. If the ride closes, the ticket is useless. The compiler needs to know: "Does this ticket expire when the first input expires? Or the second? Or both?"
When you write a function that returns a reference, you're handing out a ticket. The compiler demands a contract. The contract says: "This ticket is valid as long as these inputs are valid." If you don't write the contract, the compiler stops you. It won't guess. Guessing could hide bugs. If the compiler guessed the wrong input, you could change the function body later and the guess would be silently wrong. Explicit lifetimes force you to document the data flow.
Minimal fix: Name the lifetime
The error E0106 appears when the compiler can't infer the lifetime of a returned reference. This happens when you have multiple input references and no clear rule to pick one. The fix is to add a lifetime parameter to the function signature.
/// Returns the longer of two string slices.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// Compare lengths to decide which slice to return.
if x.len() > y.len() {
x
} else {
y
}
}
The <'a> introduces a lifetime parameter. The &'a str syntax means "a string slice that lives at least as long as 'a." By putting 'a on both inputs and the output, you tell the compiler: "The return value is valid for the intersection of the input lifetimes."
The compiler now knows the contract. If you call longest(s1, s2), the compiler calculates the scope of s1 and s2. It picks the shorter scope. That shorter scope becomes 'a. The return value is tied to that scope. If s1 lives for ten seconds and s2 lives for five seconds, the return value lives for five seconds. The reference can't outlive the data.
Lifetimes aren't durations. They're scopes. Think in terms of "who owns this", not "how long does this last".
How the lifetime parameter actually works
The lifetime parameter <'a> is a generic, just like T in Vec<T>. When you define the function, 'a is a placeholder. When you call the function, the compiler fills in the concrete lifetime.
The concrete lifetime is the intersection of the input lifetimes. The compiler checks that both inputs live at least as long as the intersection. It checks that the output lives at least as long as the intersection. If everything matches, the code compiles.
This mechanism enforces safety at the call site. The caller must keep the inputs alive for as long as they use the output. If the caller drops an input too early, the compiler rejects the code with E0597 (borrowed value does not live long enough).
fn main() {
let s1 = String::from("long string");
let result;
{
// s2 is created in this inner scope.
let s2 = String::from("short");
// longest returns a reference tied to the shorter of s1 and s2.
// s2 dies at the end of this block.
result = longest(&s1, &s2);
}
// Error: s2 is dropped, but result borrows from s2.
println!("{}", result);
}
The compiler catches this immediately. You can't use result after s2 is gone. The lifetime system prevents the dangling reference before it happens.
Add the parameter. Tie the output to the input. The error vanishes.
Realistic scenario: Independent lifetimes
Not all inputs affect the output lifetime. Sometimes a function takes multiple references but returns a reference derived from only one. In those cases, you can use separate lifetime parameters. This makes the function more flexible. It doesn't force the caller to keep unrelated data alive longer than necessary.
/// Looks up a key in the config and returns the value slice.
/// The return value depends only on config, not key.
fn lookup<'a>(key: &str, config: &'a Config) -> &'a str {
// The result comes from config.data.
// The lifetime is tied to config.
config.data.as_str()
}
struct Config {
data: String,
}
Here, key has an anonymous lifetime. It can be anything. The output lifetime 'a is tied only to config. You can call this function with a short-lived key and a long-lived config. The return value stays valid as long as config lives.
If you forced key to have the same lifetime as config, you'd over-constrain the API. Callers would have to keep key alive even though the function ignores it after the lookup. That's a constraint leak. It makes the function harder to use.
// Bad: Forces key and config to share a lifetime.
// The caller must keep key alive even though it's not needed for the result.
fn lookup_bad<'a>(key: &'a str, config: &'a Config) -> &'a str {
config.data.as_str()
}
Be precise with lifetimes. Over-constraining hurts your API more than under-constraining hurts the compiler.
Pitfalls and compiler errors
Lifetimes trip up developers in predictable ways. Knowing the common pitfalls saves hours of debugging.
Returning a reference to local data. You create a value inside the function and try to return a reference to it. The compiler stops you with E0515 (cannot return value referencing local variable). The local data dies when the function returns. The reference would point to garbage. Return the owner instead, or return a copy.
fn bad() -> &str {
let s = String::from("hello");
// Error E0515: s is dropped at the end of the function.
&s
}
Mismatched lifetimes in assignments. You try to assign a reference to a variable with a different lifetime. The compiler rejects this with E0308 (mismatched types). The lifetimes don't match. You need to adjust the scopes or the signature.
Confusing elision with multiple inputs. Rust has lifetime elision rules. If a function has exactly one input reference, the compiler infers the output lifetime from that input. If there are multiple inputs, elision fails. You must add explicit lifetimes. Don't assume elision works when it doesn't.
Static lifetimes. String literals have the 'static lifetime. They live for the entire program. You can return &'static str without tying it to any input. This is useful for constants. Don't overuse 'static. It's a special case, not a general solution.
If the compiler complains about a local reference, you're trying to return a ghost. Return the owner, or return a copy.
Decision: When to use what
Pick the right tool based on the data flow. Don't force lifetimes where ownership changes hands.
Use lifetime elision when you have exactly one input reference; the compiler infers the output lifetime automatically.
Use lifetime elision in methods with &self or &mut self; the output lifetime ties to the receiver.
Use explicit lifetime parameters when you have multiple input references and the compiler can't guess which one the output depends on.
Use separate lifetime parameters when inputs have independent validity scopes and the output depends on only one of them.
Use Cow<str> when you sometimes return a borrowed slice and sometimes return an owned String; this avoids lifetime annotations entirely by boxing the return type.
Use String return types when the function creates new data or merges inputs; returning an owned value is simpler than managing lifetimes.
Pick the tool that matches the data flow. Don't force lifetimes where ownership changes hands.