The compiler rejects your reference
You write a function that finds the longest string in a list. It feels simple. You pass in two strings, compare lengths, and return the winner. The compiler rejects you with a wall of text about lifetimes. You stare at the code. It looks fine. The strings are right there. Why does Rust think you're returning a reference to nothing?
The error isn't a bug in Rust. It's a mismatch between your mental model of where data lives and what the compiler can prove. Rust requires every reference to point to valid memory. If there's any chance the data gets destroyed while the reference still exists, the code won't compile. Lifetimes are the tool Rust uses to track this guarantee. Debugging lifetime issues means learning to read the compiler's proof and adjusting your code to satisfy it.
Lifetimes are promises about scope
A reference in Rust is a pointer to data, but it carries a contract. That contract is the lifetime. The lifetime tells the compiler how long the reference is valid. It's a promise that the data behind the reference will not be dropped for at least that long.
Think of a reference as a leash. The lifetime is the length of the leash. The data is the dog. If the dog runs out of the leash's reach, the owner is in trouble. Rust ensures the leash is always long enough. If you try to hand someone a leash while the dog is about to run away, Rust stops you.
Lifetimes are not runtime constructs. They do not exist when your program runs. They are compile-time annotations that help the borrow checker verify safety. The cost of lifetimes is your time writing them, not CPU cycles. Once the code compiles, the lifetimes vanish. The generated machine code contains no lifetime metadata.
A lifetime is a guarantee. If you can't guarantee the data stays alive, you can't hand out the reference.
Minimal example: connecting inputs to outputs
The most common lifetime error happens when a function takes references as input and returns a reference as output. The compiler needs to know which input the output is tied to. Without that information, it assumes the output might refer to a local variable that disappears when the function returns.
Here is a function that returns the longer of two string slices.
/// Returns the longer of two string slices.
fn longest(x: &str, y: &str) -> &str {
// Compare lengths to decide which slice to return.
if x.len() > y.len() {
x
} else {
y
}
}
The compiler rejects this with E0106 (missing lifetime specifier). It sees two input references and one output reference. It doesn't know if the output refers to x, y, or some local variable created inside the function. If the output referred to a local variable, the reference would dangle immediately after the function returns. The compiler refuses to guess.
You fix this by adding a lifetime parameter. The parameter connects the inputs to the output.
/// Returns the longer of two string slices, tied to lifetime 'a.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
// Both inputs and the output share the same lifetime 'a.
if x.len() > y.len() {
x
} else {
y
}
}
The 'a is a lifetime variable. It says "there exists some scope 'a such that x lives for 'a, y lives for 'a, and the return value lives for 'a." The compiler now knows the output is tied to the inputs. It checks that the caller provides references that live long enough.
Convention: Use 'a for the first lifetime parameter. If you have multiple, use 'b, 'c. Descriptive names like 'input help in complex functions, but 'a is the standard for simple cases.
The annotation ties the output to the inputs. The compiler now knows the reference is safe.
Walkthrough: how the compiler checks the contract
When you call longest, the compiler substitutes the actual lifetimes of the arguments into the function signature. It checks that the return value's lifetime matches the constraint.
fn main() {
// String lives for the entire main function.
let string1 = String::from("long string");
// String lives for the entire main function.
let string2 = String::from("short");
// Call longest with references to string1 and string2.
let result = longest(&string1, &string2);
// result is valid because string1 and string2 are still alive.
println!("Longest is: {}", result);
}
The compiler sees that string1 and string2 both live until the end of main. It infers that 'a must be at least as long as the scope where result is used. Since the strings outlive result, the check passes.
Now consider a case where the lifetimes clash.
fn main() {
let result;
{
// string1 lives only inside this block.
let string1 = String::from("long string");
// string2 lives until the end of main.
let string2 = String::from("short");
// longest requires both inputs to share a lifetime.
result = longest(&string1, &string2);
}
// string1 is dropped here.
// result refers to string1, which is gone.
println!("Longest is: {}", result);
}
The compiler rejects this with E0597 (borrowed value does not live long enough). It points out that string1 is dropped at the end of the block, but result is used after the block. The lifetime 'a must cover the usage of result, but string1 doesn't live that long. The contract is broken.
To fix this, you either extend the scope of string1 or change the function to return an owned value. Lifetimes cannot extend the life of data. They can only describe how long data lives.
The compiler found a dangling pointer before it happened. Fix the scope or take ownership.
Realistic example: structs and methods
Lifetimes appear frequently in structs that hold references. A struct that stores a reference must declare how long that reference must live. This prevents the struct from outliving the data it points to.
/// A struct that holds a reference to a string.
struct Quote<'a> {
/// The text being quoted.
text: &'a str,
}
impl<'a> Quote<'a> {
/// Returns the quoted text.
fn get_text(&self) -> &'a str {
// Return the text reference, preserving the lifetime.
self.text
}
}
The struct definition includes <'a>. The field text uses &'a str. This means any Quote instance must hold a reference that lives for at least 'a. The impl block also declares <'a> so it can work with the lifetime parameter.
Here is how you use this struct safely.
fn main() {
// novel lives for the entire main function.
let novel = String::from("Call me Ishmael.");
// Create a quote referencing the novel.
let quote = Quote { text: &novel };
// This works because novel outlives quote.
println!("{}", quote.get_text());
}
The compiler infers that 'a is the scope of novel. Since quote is created inside that scope and used inside that scope, everything is valid.
If you try to keep the quote after the novel is dropped, the compiler stops you.
fn main() {
let quote;
{
let novel = String::from("Call me Ishmael.");
// This fails: novel is dropped at the end of the block.
quote = Quote { text: &novel };
}
// quote.text is dangling here.
println!("{}", quote.get_text());
}
The error is E0597 again. The struct holds a reference, so the struct cannot outlive the reference. The lifetime annotation on the struct makes this constraint explicit.
Structs that hold references must declare their lifetimes. The struct cannot outlive the data it points to.
Pitfalls and common errors
Lifetime errors fall into a few patterns. Recognizing the pattern helps you fix the issue faster.
Returning a reference to a local variable. This is the most dangerous mistake. If you create data inside a function and return a reference to it, the data is dropped immediately. The reference becomes a dangling pointer.
fn dangerous() -> &str {
let s = String::from("hello");
// Returns reference to s, which is dropped immediately.
&s
}
The compiler rejects this with E0515 (cannot return reference to local variable). The data dies when the function returns. Rust forbids this. The fix is to return an owned value instead.
fn safe() -> String {
let s = String::from("hello");
// Return the owned String.
s
}
Conflicting lifetime requirements. Sometimes you have multiple references with different lifetimes, and the compiler cannot satisfy all constraints. This often happens when you try to mutate data while holding a reference to it.
fn main() {
let mut data = String::from("hello");
// Borrow data immutably.
let ref1 = &data;
// Borrow data mutably.
let ref2 = &mut data;
// This fails: cannot borrow as mutable because it is also borrowed as immutable.
ref2.push_str(" world");
}
This is E0502 (cannot borrow as mutable because it is also borrowed as immutable). It's a borrow checker error, but it feels like a lifetime issue. The fix is to drop the immutable borrow before creating the mutable borrow.
Lifetime hell. Adding too many lifetime parameters makes code hard to read and use. If you find yourself writing fn foo<'a, 'b, 'c, 'd>(...), you might be overusing references. Owned types often simplify the design.
Convention: When you see 'static, think "lives for the entire program". String literals have this lifetime. Do not confuse 'static with static variables; it's a lifetime bound, not a storage class. Using 'static restricts your function to only accept data that lives forever. Use it sparingly.
When the compiler complains about lifetimes, look for the data that dies too early. Extend the scope or take ownership.
Decision: when to use lifetimes vs alternatives
Lifetimes are powerful, but they are not the only tool. Choosing the right approach depends on your data flow.
Use explicit lifetime annotations when the compiler cannot infer the relationship between inputs and outputs, such as a function returning one of its reference arguments. Use explicit lifetimes when a struct holds references, because the struct definition must declare how long those references must live. Reach for owned types like String or Vec when you need to move data across scopes without worrying about reference validity; ownership solves lifetime problems by making the data travel with the value. Rely on lifetime elision when the pattern is simple: one input reference maps to one output reference, or a method takes &self and returns a reference. Trust the compiler's elision rules for standard patterns; adding annotations there adds noise without safety.
Lifetimes are compile-time checks. They cost zero at runtime. Use them to share data without copying, but reach for ownership when the complexity grows.