What Rust lifetimes actually mean
Lifetimes are the part of Rust people bounce off. The 'a syntax looks like a spell you copy until the compiler stops shouting. It is not a spell. A lifetime is the compiler answering one question: for how long is this reference guaranteed to point at valid data?
The one idea
A reference is a borrow: it points at data it does not own. The danger is obvious, if the data is freed while the reference still exists, you have a dangling pointer, the bug behind a huge fraction of security vulnerabilities in C and C++. Rust's rule is simple: a reference must never outlive the data it points to. Lifetimes are how the compiler proves that rule holds, at compile time, with no runtime cost.
See the error first
This looks harmless and does not compile:
fn main() {
let r;
{
let x = 5;
r = &x; // r borrows x
} // x is dropped here
println!("{}", r); // r now points at freed memory
}
error[E0597]: `x` does not live long enough
| r = &x;
| ^^ borrowed value does not live long enough
| }
| - `x` dropped here while still borrowed
In C this compiles and reads freed memory. In Rust the borrow checker notices that r is used after x is gone and refuses. You did not write a single 'a, yet lifetimes did their whole job: they tracked that &x is only valid inside the inner block.
Where the annotation comes from
Most of the time the compiler infers lifetimes and you never type one. You are forced to annotate only when it cannot tell how the lifetimes of several references relate. The classic case is returning a reference:
fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}
error[E0106]: missing lifetime specifier
| fn longest(a: &str, b: &str) -> &str {
| ^ expected named lifetime parameter
The compiler's problem is honest: the returned reference borrows from a or b, and it needs to know that the result cannot outlive whichever one it came from. You tell it with a name:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
Read 'a as a label, not a duration. It says: the returned reference lives at least as long as both inputs. You are not setting the lifetime, you are describing the relationship so the caller's uses can be checked. If a caller tries to keep the result after one of the inputs is gone, that call site fails to compile, exactly where the bug would be.
The relationship is the point
This compiles, because both s1 and the borrow of s2 outlive the println!:
let s1 = String::from("longer string");
let result;
{
let s2 = String::from("short");
result = longest(s1.as_str(), s2.as_str());
println!("{}", result); // used while s2 is still alive
}
Move the println! outside the inner block, so result (possibly borrowing s2) is used after s2 is dropped, and the compiler rejects it, again pointing at the exact line. The annotation did not add a runtime check. It gave the compiler enough information to check something it otherwise could not.
The mental model that makes it click
Stop reading lifetimes as "how long something lives" and start reading them as "which references must stay valid together." 'a on two parameters and a return type means "these three are tied; none of the borrows may end while the others are in use." Once you see them as a contract between references, the errors read like a helpful colleague: this borrow could outlive its data, and here is where.
Lifetimes are Rust trading a little annotation for a guarantee C spends decades of CVEs failing to make: no use-after-free, checked before the program ever runs. Getting fluent in reading these errors, rather than fearing them, is the whole arc of the Rust track, where the borrow checker stops being an adversary and starts being the thing that lets you write concurrent code without flinching.