Lifetimes
In one sentence
Section titled “In one sentence”A lifetime annotation like 'a tells the compiler how references relate to each other; it never makes a value live longer.
The Java you know
Section titled “The Java you know”In Java, a reference keeps its object alive. If a method returns one of its arguments, the caller can keep the result for as long as it wants. The garbage collector does not free the object while any reference to it exists.
Java
static String longer(String first, String second) { return second.length() > first.length() ? second : first;}
static String demo() { String name; { String nickname = "Ada"; name = longer("Ada Lovelace", nickname); } // `nickname` is out of scope, but its object lives on return name;}Rust
pub fn longer<'a>(first: &'a str, second: &'a str) -> &'a str { if second.len() > first.len() { second } else { first }}Rust has no garbage collector, so a reference does not keep its value alive. The value is freed when its owner goes out of scope (see Ownership and moves). The compiler must prove that no reference is used after that point. Lifetimes are how it does this.
The Rust you will see
Section titled “The Rust you will see”A lifetime is a name for “the region of code where this reference is valid”. It starts with an apostrophe: 'a, 'b, 'de, 'static. Most code has no lifetime annotations at all. You see them where a function returns a reference, and the compiler cannot guess which input it comes from:
fn longer(first: &str, second: &str) -> &str { // error E0106: missing lifetime specifier if second.len() > first.len() { second } else { first }}Two references go in, and one comes out. Is the result valid as long as first, or as long as second? The compiler does not look at the body to decide. It looks only at the signature, and the signature does not say. With the annotation, it does:
pub fn longer<'a>(first: &'a str, second: &'a str) -> &'a str { if second.len() > first.len() { second } else { first }}- Line 1:
<'a>declares a lifetime parameter, like a generic type parameter.&'a stris "a string slice that is valid for at least'a". The result has the same'a: it is valid only while **both** inputs are valid. - Line 2-6: The body is normal code. Lifetimes change nothing at run time. They exist only for the compiler.
Read fn longer<'a>(first: &'a str, second: &'a str) -> &'a str as: “the result borrows from first or second, so do not use it after either of them is gone”. The compiler uses this to reject code like the following, where nickname is freed while name can still point to it:
let customer = String::from("Ada Lovelace");let name;{ let nickname = String::from("Ada"); name = longer(&customer, &nickname); // error E0597: `nickname` does not live long enough} // `nickname` is dropped here, but `name` may still point to itprintln!("{name}");In Java this is fine, because name keeps the string alive. In Rust, the fix is to change the scopes, or to return an owned String instead of a reference.
How to read it
Section titled “How to read it”Lifetime elision: why most code has no annotations
Section titled “Lifetime elision: why most code has no annotations”The compiler fills in lifetimes for common cases. This is called lifetime elision. The rules, simplified:
- Each reference parameter gets its own lifetime.
- If there is exactly one reference parameter, the output gets its lifetime.
- If one of the parameters is
&selfor&mut self, the output gets the lifetime ofself.
So fn first_word(text: &str) -> &str needs no annotation: there is one input reference, so the result borrows from it. A method fn name(&self) -> &str needs none either: the result borrows from self. You need annotations only when the rules do not give an answer, as in longer.
'static
Section titled “'static”'static is the lifetime of the whole program. You see it in two places, and they mean different things:
&'static str: a reference that is valid until the program ends. String literals have this type, because they are part of the program’s binary.T: 'staticas a bound: the typeTcontains no references that could become invalid. An ownedStringorVec<u64>is'staticin this sense. It does not mean “lives forever”. It means “does not borrow anything short-lived”.
pub const DEFAULT_CURRENCY: &str = "EUR"; // a literal is `&'static str`thread::spawn requires F: 'static, because the new thread can run longer than the function that started it. So the closure must own its data, not borrow it. That is what move does:
/// `thread::spawn` needs `F: 'static`: the closure must not borrow/// anything from the caller's stack, because the thread can outlive it.pub fn count_in_background(lines: Vec<String>) -> thread::JoinHandle<usize> { thread::spawn(move || lines.len()) // `move`: the thread owns `lines`}Structs that hold references
Section titled “Structs that hold references”A struct can hold a reference if it declares a lifetime parameter. The 'a says: a value of this struct cannot outlive the data that it borrows.
/// Reads fields from a line without copying it. The `'a` says: a/// `LineParser` cannot outlive the text that it borrows.pub struct LineParser<'a> { rest: &'a str,}
impl<'a> LineParser<'a> { pub fn new(line: &'a str) -> Self { Self { rest: line } }
pub fn next_field(&mut self) -> Option<&'a str> { if self.rest.is_empty() { return None; } let (field, rest) = self.rest.split_once(',').unwrap_or((self.rest, "")); self.rest = rest; Some(field) }}This is valid, and it is a common pattern for parsers, which read a large input without copying it. But it is a design choice with a cost: every function that stores or returns a LineParser must also handle 'a, and the parser cannot be kept after the input is freed. When you see a struct with a lifetime parameter in AI-generated code, ask: why does this struct not own its data? If the answer is “to save a copy of a short string”, an owned String is usually the better design.
What good looks like
Section titled “What good looks like”- No annotations where elision works. Idiomatic code has lifetime annotations only where the compiler needs them.
- Owned data in long-lived structs. Structs that live a long time (configuration, domain objects, cache entries) own their fields. Borrowing structs are short-lived helpers: parsers, iterators, views.
'staticonly on real constants, andT: 'staticbounds only where the API needs them (threads, global registries).- Owned return values when the borrow is awkward. If a function returns a reference with a complex lifetime, an owned
StringorVeccan be simpler, and is often fast enough.
Red flags in AI output
Section titled “Red flags in AI output”- Annotations that elision makes unnecessary.
fn first_word<'a>(text: &'a str) -> &'a str. Correct, but noise. Clippy reports it (needless_lifetimes). See S21: Unnecessary lifetime annotations, or'staticto silence errors. - Unused lifetime parameters.
<'a, 'b>on a function where one of them is not used in the output. The agent guessed. 'staticadded to make an error go away. The compiler says “borrowed value does not live long enough”, and the agent changes&'a strto&'static str. Now only literals work, and the next caller with a runtime string has a problem.Box::leakto produce a&'staticvalue. It compiles because the memory is never freed. In a loop or a server, this is a memory leak:
/// Makes the error go away by leaking memory: the `String` is never freed.pub fn config_name(name: String) -> &'static str { Box::leak(name.into_boxed_str())}- A struct with a lifetime parameter that is stored for a long time, for example in a cache or in application state. The lifetime spreads to every type that holds it.
- S21Severity: LowUnnecessary lifetime annotations, or
'staticto silence errorsAnnotations that elision makes unnecessary, or a'staticbound added to stop an error.
Better vs worse
Section titled “Better vs worse”The task: two small helpers that read parts of an order line.
WorseExplicit lifetimes that elision gives for free, and an unused parameter with its own lifetime.
pub fn first_word<'a>(text: &'a str) -> &'a str {text.split_whitespace().next().unwrap_or("")}pub fn customer_name<'a, 'b>(order_line: &'a str, _separator: &'b str) -> String {order_line.split(',').nth(1).unwrap_or("").to_string()}Run in Playground (opens the Rust Playground in a new tab) BetterNo annotations: the elision rules give the same meaning. The unused parameter is gone.
pub fn first_word(text: &str) -> &str {text.split_whitespace().next().unwrap_or("")}pub fn customer_name(order_line: &str) -> String {order_line.split(',').nth(1).unwrap_or("").to_string()}Run in Playground (opens the Rust Playground in a new tab)
Both versions compile, and the meaning of first_word is identical. The cost of the “worse” version is reading time, and a false signal: explicit lifetimes tell the reader “something unusual happens here”. When nothing unusual happens, the reader looks for a problem that is not there.
Explicit annotations are correct when the elision rules do not apply (two input references and a reference output, as in longer), or when the author wants to show that the result is tied to one specific input.
Check yourself
Section titled “Check yourself”What does
'ainfn longer<'a>(first: &'a str, second: &'a str) -> &'a strchange at run time?Show the answer
Nothing. Lifetimes exist only at compile time. The annotation tells the compiler that the result is valid only while both inputs are valid, so it can reject code that uses the result too late.
Does
fn name(&self) -> &strneed a lifetime annotation?Show the answer
No. With
&self, the elision rules give the output the lifetime ofself. The method returns a borrow of the object.Is a
Stringvalue'static?Show the answer
As a type bound, yes:
String: 'staticis true, because aStringborrows nothing. It does not live forever. It is freed when its owner goes out of scope.An agent fixed a lifetime error with
Box::leak. What do you ask?Show the answer
How often this code runs. Each call leaks memory that is never freed. Ask the agent to change the type that needs
&'static strto own aString, or to restructure the code so that the value lives long enough.