When the compiler asks for more
You write a function that takes two strings and returns the longer one. The logic is trivial. You compare lengths and return the winner. You run cargo run and the compiler rejects the code. It complains that it cannot infer an appropriate lifetime for the return value. You stare at the screen. The strings are passed in. The result is one of them. The relationship seems obvious. Why does the compiler need more help?
This is the moment every Rust beginner hits. The borrow checker stops you because multiple references are involved, and the default rules aren't enough to guarantee safety. The compiler isn't being difficult. It is protecting you from a dangling reference that would crash your program at runtime. The fix is to add explicit lifetime annotations. These annotations tell the compiler exactly how the output reference relates to the input references.
Lifetimes are names for scopes
A reference in Rust is a borrow. A borrow has a duration. That duration is called a lifetime. When you see &'a str, the 'a is not magic. It is just a name for a scope. It tells the compiler that this reference is valid for some duration 'a.
Think of a reference as a ticket to a ride at a theme park. The ticket is only valid while the ride is open. If the ride closes, the ticket becomes trash. When you write a function that returns a reference, you are handing out a ticket. The compiler needs to know which ride the ticket belongs to. It needs to check that the ride stays open as long as anyone holds the ticket.
If a function takes two references, there are two rides. The compiler doesn't know if the returned ticket belongs to the first ride, the second ride, or some third ride that isn't mentioned. If it guesses wrong, someone might hold a ticket after the ride has closed. That is a dangling reference. In C or C++, this leads to undefined behavior and segfaults. In Rust, the compiler refuses to compile the code until you clarify the relationship.
Lifetimes do not change how long things live. They only describe how long they live. Adding a lifetime annotation never extends the life of a value. It only helps the compiler verify that the value lives long enough.
The minimal case
Consider the longest function. It takes two string slices and returns the longer one. Without annotations, the signature looks like this:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
The compiler rejects this with E0106 (missing lifetime specifier). It sees two input lifetimes and one output lifetime. It cannot assume the output is tied to either input. It stops and asks you to specify.
You fix it by introducing a lifetime parameter 'a and attaching it to all the references:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// Compare lengths and return the reference to the longer string.
if x.len() > y.len() { x } else { y }
}
The <'a> declares that this function works with some lifetime 'a. The &'a str types say that x, y, and the return value all share that lifetime. The compiler now knows the returned reference is valid as long as both inputs are valid. More precisely, it is valid for the shorter of the two input lifetimes. If x lives for 10 seconds and y lives for 5 seconds, the result lives for 5 seconds.
Convention aside: use 'a for simple cases like this. If you have multiple distinct lifetimes, switch to descriptive names like 'input or 'data. The community prefers clarity when the complexity rises. For a single lifetime parameter, 'a is the standard.
The annotation doesn't change the code. It changes the contract.
What the compiler is checking
When you call longest, the compiler checks the lifetimes at the call site. Suppose you have two string slices with different scopes:
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
// s2 is borrowed here. Its lifetime ends at the closing brace.
result = longest(&s1, &s2);
}
// s2 is dropped here. The borrow is no longer valid.
println!("The longest string is {}", result);
}
This code fails to compile. The compiler rejects it with E0597 (s2 does not live long enough). The function longest promised that the result lives as long as both inputs. s2 dies when the inner block ends. result tries to outlive s2. The borrow checker catches the mismatch.
You can fix this by moving the use of result inside the block, or by ensuring s2 lives long enough. The lifetime annotation forced the compiler to track the dependency. Without it, the compiler might have allowed the code, and you would have printed garbage or crashed.
Lifetimes are erased at runtime. There is zero performance cost. The annotations exist only to help the compiler prove safety. Once the proof is complete, the generated machine code contains no lifetime checks. You pay for safety at compile time, not at runtime.
Trust the borrow checker. If it asks for a lifetime, the relationship isn't clear enough for the defaults.
Real code with structs
Lifetimes appear frequently in structs that store references. A struct that holds a reference cannot outlive the data it points to. You must annotate the struct to express this constraint.
/// A document that borrows its content from external storage.
struct Document<'a> {
title: &'a str,
content: &'a str,
}
impl<'a> Document<'a> {
/// Returns the title of the document.
fn get_title(&self) -> &'a str {
self.title
}
}
The <'a> on the struct declares that Document depends on a lifetime 'a. The fields title and content use &'a str, meaning they borrow data that lives at least as long as 'a. The impl block also needs <'a> so it can use the lifetime in methods.
When you create a Document, the compiler ensures the borrowed strings live long enough:
fn main() {
let source = String::from("Rust is safe");
let doc = Document {
title: "Intro",
content: &source,
};
// doc is valid here because source is still alive.
println!("Content: {}", doc.content);
// source is dropped here. doc cannot be used after this point.
}
If you try to use doc after source is dropped, the compiler stops you. The lifetime annotation on the struct enforces the rule that the borrower cannot outlive the owner.
Keep lifetimes in the signature. They belong on the type, not buried in the logic.
Pitfalls and myths
New Rustaceans often fall into a few traps when working with lifetimes. The most common is the belief that adding a lifetime annotation makes data live longer. It does not. Lifetimes describe reality. They never create it. If you try to return a reference to a local variable, adding 'a won't save you.
fn bad_function<'a>() -> &'a str {
let s = String::from("hello");
// This fails. s is dropped at the end of the function.
// The lifetime annotation cannot extend s's life.
&s
}
The compiler rejects this with E0515 (cannot return reference to local variable). The annotation says the result should live for 'a, but s lives only for the function scope. There is no way to satisfy the contract. You must return an owned String or a reference to data that lives outside the function.
Another pitfall is over-annotating. Rust has lifetime elision rules that let you skip annotations in simple cases. If a function has exactly one input reference, the compiler assumes the output reference shares that lifetime. You don't need explicit annotations there.
fn first_word(s: &str) -> &str {
// Elision applies. The output lifetime matches the input lifetime.
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' { return &s[..i]; }
}
&s[..]
}
This compiles fine. Adding <'a> here is redundant. The compiler infers it automatically. Use explicit annotations only when elision fails, which happens when there are multiple input references or when the relationship isn't one-to-one.
Convention aside: run cargo clippy on your code. Clippy often suggests removing redundant lifetime annotations or adding missing ones. It helps keep your signatures clean and idiomatic.
Lifetimes describe reality. They never create it. If you need data to live longer, you need to own it.
When to annotate
Use explicit lifetime annotations when a function returns a reference and takes multiple input references, so the compiler knows which input the output is tied to. Use explicit lifetime annotations on structs that hold references, because the struct must not outlive the data it points to. Rely on lifetime elision when a function has exactly one input reference, since the compiler automatically assigns that lifetime to the output. Reach for owned types like String when the result needs to live longer than the input data, or when you are creating new data inside the function. Use &'static str only for string literals hardcoded in your source, not for data that comes from users or files.
Pick the tool that matches the data flow. Annotate when the compiler needs clarity. Own when you need longevity.