Why Lifetimes Are Necessary
Lifetimes are necessary in Rust to prevent dangling pointers (use-after-free bugs) at compile time without needing a runtime garbage collector. Furthermore, explicit lifetime annotations ('a) are necessary on function signatures and struct definitions whenever a returned reference or stored reference could plausibly borrow from more than one source, allowing rustc to verify callers and callees independently.
Problem 1: Preventing Dangling References Locally
In C and C++, a pointer can easily outlive the stack variable or heap buffer it points to. If a function returns the address of a local stack variable (return &local_var;), the stack frame is destroyed on return and the caller receives a pointer to garbage memory.
By tracking the lifetime of every value and reference, Rust catches dangling references at compile time before the program can ever run:
// ❌ does not compile
fn create_dangling_reference() -> &String {
let local = String::from("temporary stack String");
&local // Error: `local` is dropped at `}`, so `&local` would dangle!
}
fn main() {}
error[E0106]: missing lifetime specifier
--> src/main.rs:1:35
|
1 | fn create_dangling_reference() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`, or if you really want a 'static reference
|
1 | fn create_dangling_reference() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
1 - fn create_dangling_reference() -> &String {
1 + fn create_dangling_reference() -> String {
|
error: aborting due to 1 previous error
Even if you annotate -> &'static String, the compiler rejects &local with E0515 (cannot return reference to local variable 'local'). Returning the owned String by value (-> String) transfers ownership safely to the caller instead.
Problem 2: Why Function Signatures Need Lifetime Annotations
Remember that Rust checks each function locally against its signature without inspecting the bodies of other functions.
Suppose you write a function longest(x: &str, y: &str) -> &str that takes two string slices and returns the longer one:
Caller Scope (`main`):
let string1 = String::from("long string"); (Lives for outer scope `'long`)
{
let string2 = String::from("xyz"); (Lives only for inner scope `'short`)
let result = longest(&string1, &string2);
} // <-- `string2` is DROPPED here!
println!("{result}"); // <-- Is `result` still valid here?
Look at longest(&string1, &string2) from the compiler's perspective when checking main:
- If
longestalways returned its first argument (x),resultwould borrowstring1('long) andprintln!("{result}")after the inner block would be safe. - If
longestcan return eitherxorydepending on runtime lengths, thenresultmight point tostring2('short), so usingresultafterstring2is dropped would be a use-after-free bug!
Because rustc refuses to guess which input parameter (x or y) the returned &str borrows from by secretly reading longest's body, longest without lifetime annotations fails to compile:
// ❌ does not compile
fn longest(x: &str, y: &str) -> &str {
if x.len() >= y.len() { x } else { y }
}
fn main() {}
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
error: aborting due to 1 previous error
How Lifetime Annotations Solve the Ambiguity
Adding <'a> to the signature (fn longest<'a>(x: &'a str, y: &'a str) -> &'a str) establishes an explicit contract between longest and its callers:
- Contract on
longest's body: The returned&strmay come from eitherxory. - Contract on callers of
longest: The returned reference is guaranteed to remain valid for the overlap (shorter) ofx's lifetime andy's lifetime!
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
// Both `string1` and `string2` are alive inside this inner block, so `result` is valid here:
let result = longest(&string1, &string2);
println!("Longest string is: '{result}'");
}
}
Longest string is: 'long string is long'
Problem 3: Why Structs Holding References Need Lifetimes
If a struct stores a reference field (such as struct Excerpt<'a> { text: &'a str }), the struct instance itself must never outlive the String that its text field borrows from. Annotating 'a on the struct allows the borrow checker to tie the struct's validity directly to the borrowed source buffer.