Lifetimes
A lifetime is the scope during which a reference is valid. Rust’s borrow checker uses lifetimes to verify that references never outlive their referents — preventing dangling references at compile time. In most cases, lifetimes are elided — the compiler infers them automatically from a small set of rules. For more elaborate cases (functions returning references, struct types containing references), explicit lifetime parameters are required. The mechanism is one of Rust’s most distinctive — and most initially confusing — features; mastering it is part of fluency in the language.
This page covers the lifetime concept, lifetime elision, explicit lifetime parameters, the 'static lifetime, and the principal patterns. The deeper interaction with ownership is in Ownership and borrowing; the relationship to traits is in Traits.
Why lifetimes exist
Without lifetimes, a function returning a reference could produce dangling references:
// Hypothetical (does not compile):
fn longer(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}
let result;
{
let s = String::from("hello");
result = longer(&s, "world"); // result borrows from s
} // s is dropped
println!("{}", result); // would access freed memory
The borrow checker rejects the program because result would outlive s. The mechanism is lifetimes — annotations that admit the compiler to verify the reference relationships.
The actual signature requires explicit lifetime parameters:
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
The 'a is a lifetime parameter; it ranges over actual lifetimes. The signature reads: “for any lifetime 'a, given two &str references with that lifetime, return a &str with that lifetime”. The compiler then verifies that the actual call sites supply references whose lifetimes are at least 'a.
Lifetime elision
For simple cases, the compiler infers lifetimes through three elision rules:
- Each input reference parameter gets its own lifetime parameter (
fn f(s: &str)becomesfn f<'a>(s: &'a str)). - If there is exactly one input lifetime, it is assigned to all output lifetimes (
fn f(s: &str) -> &strbecomesfn f<'a>(s: &'a str) -> &'a str). - If a method has
&selfor&mut self, theself’s lifetime is assigned to all output lifetimes.
These rules cover the substantial majority of cases; explicit lifetime parameters are needed only when the elision is insufficient.
// Rule 1 + 2 apply:
fn first(s: &str) -> &str { // elided: fn first<'a>(s: &'a str) -> &'a str
&s[0..1]
}
// Rule 3 applies:
impl Person {
fn name(&self) -> &str { // elided: fn name<'a>(&'a self) -> &'a str
&self.name
}
}
// No rule applies:
fn longer(a: &str, b: &str) -> &str { // ERROR: cannot determine output lifetime
/* ... */
}
// Explicit:
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str { /* ... */ }
The conventional discipline: trust elision for simple cases; add explicit lifetimes only when the compiler requires.
Explicit lifetime parameters
Lifetime parameters are introduced before the type parameters in angle brackets:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
The 'a is a name; the convention is short lowercase names ('a, 'b, 'src, 'ctx). The lifetime appears alongside the references it constrains.
Multiple lifetime parameters admit independent lifetimes:
fn first<'a, 'b>(a: &'a str, b: &'b str) -> &'a str {
a // returns from a; b's lifetime is independent
}
The function signature reads: “for any lifetimes 'a and 'b, given references with those lifetimes, return a reference with 'a”. The compiler verifies that a and the return value share a lifetime.
Lifetime bounds
Lifetimes can be constrained relative to each other:
fn longer_outlives<'a, 'b>(a: &'a str, b: &'b str) -> &'b str
where
'a: 'b, // 'a outlives 'b
{
a // OK: 'a outlives 'b, so &'a is &'b-valid
}
The 'a: 'b reads “'a outlives 'b” — i.e., 'a is at least as long as 'b. The mechanism admits expressing complex relationships between references.
For most code, the constraint is implicit through usage; explicit bounds are needed for libraries that expose elaborate generic interfaces.
Lifetimes in structs
A struct that holds references must declare lifetime parameters:
struct Parser<'a> {
input: &'a str,
position: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser { input, position: 0 }
}
fn next(&mut self) -> Option<&'a str> {
// ...
}
}
The struct Parser<'a> admits any lifetime; instances borrow from a &str that lives at least as long as the parser. The borrow checker verifies the constraint.
For structs that hold owned data, no lifetime parameter is needed:
struct OwnedParser {
input: String, // owned, no lifetime
position: usize,
}
The owned form admits the parser to outlive any particular borrowed source; the trade-off is that callers cannot share the source freely.
The 'static lifetime
The 'static lifetime is special — it represents “lives for the entire program”. The principal sources:
- String literals —
&'static strwith the program’s lifetime. staticitems — declared at the top level, live for the program’s duration.- Heap-allocated values explicitly leaked (
Box::leak).
let s: &'static str = "hello"; // string literal
let n: &'static i32 = &42; // since 1.21: literal-with-static-promotion
static GREETING: &str = "Hello, world!";
let g: &'static str = GREETING;
The 'static is also a bound — a constraint that admits any type that does not borrow from anything (or borrows only 'static):
fn store<T: 'static>(value: T) {
// value can be stored anywhere; it doesn't borrow from any particular scope
}
The T: 'static constraint reads “T does not contain references with shorter lifetimes” — the type either owns its data or borrows only 'static. The mechanism admits storing values in long-lived contexts (a thread, a global, a channel).
The 'static is not the same as “the program’s data” — it’s a lifetime bound, meaning “lives at least this long”. A String is 'static (it owns its data); a &'a str for some 'a != 'static is not.
Generic lifetime parameters
Lifetimes are generic parameters alongside type parameters:
struct Container<'a, T> {
items: &'a [T],
}
impl<'a, T: Clone> Container<'a, T> {
fn first(&self) -> Option<T> {
self.items.first().cloned()
}
}
The <'a, T> introduces both a lifetime and a type parameter. The function admits any lifetime and any cloneable type.
Lifetime inference in scopes
The compiler reasons about lifetimes within function bodies:
fn main() {
let s = String::from("hello");
let r;
{
let t = String::from("world");
r = if s.len() > t.len() { &s } else { &t };
println!("{}", r); // OK: r still borrows valid data
}
// println!("{}", r); // ERROR: r might borrow from t (now dropped)
}
The borrow checker tracks each reference’s source; if any path admits the reference to outlive its source, the program is rejected.
The conventional fix is to scope the references appropriately or to take ownership:
let s = String::from("hello");
let t = String::from("world");
let r = if s.len() > t.len() { &s } else { &t };
println!("{}", r); // OK: both s and t are still alive
Higher-ranked trait bounds (HRTB)
For functions accepting closures or trait objects that work for any lifetime:
fn apply_to_all<F>(f: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
f("a");
f("bbb");
}
The for<'a> reads “for all lifetimes 'a” — the closure works for any lifetime. The mechanism is necessary when the lifetime cannot be fixed at the function’s declaration (the closure may be called with arguments of varying lifetimes).
The conventional uses are advanced; most Rust code does not encounter HRTBs.
Common patterns
Function returning a reference into its argument
fn first_word(s: &str) -> &str { // lifetime elided
s.split_whitespace().next().unwrap_or("")
}
The single input lifetime is propagated to the output; no explicit annotation needed.
Method returning a field reference
struct Wrapper {
value: String,
}
impl Wrapper {
fn value(&self) -> &str { // returns a borrow into self
&self.value
}
}
The &self’s lifetime applies to the return value; the elision rules cover this.
Struct with a reference
struct Cursor<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> Cursor<'a> {
fn new(data: &'a [u8]) -> Self {
Cursor { data, pos: 0 }
}
}
The struct’s lifetime parameter admits borrowing data; the implementation block introduces the same parameter.
Multi-input function with diverging lifetimes
fn select<'a>(a: &'a str, b: &str) -> &'a str {
a // returns from a only
}
The b parameter has its own (elided) lifetime; the return value is tied to a only.
'static-bound functions
fn spawn_thread<F>(f: F)
where
F: FnOnce() + Send + 'static,
{
std::thread::spawn(f);
}
The 'static bound admits the closure to outlive the calling stack frame — it can be moved to a thread that runs longer than the caller.
Returning a borrow from a longer-lived source
struct Document {
sections: Vec<Section>,
}
impl Document {
fn first_section(&self) -> Option<&Section> {
self.sections.first()
}
}
The &self’s lifetime admits the returned reference to be valid as long as self is.
Iterators with lifetimes
struct Lines<'a> {
remaining: &'a str,
}
impl<'a> Iterator for Lines<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
if self.remaining.is_empty() {
None
} else {
let (line, rest) = match self.remaining.find('\n') {
Some(pos) => (&self.remaining[..pos], &self.remaining[pos+1..]),
None => (self.remaining, ""),
};
self.remaining = rest;
Some(line)
}
}
}
The iterator yields references that live as long as the source. The lifetime parameter on Lines<'a> and Item = &'a str admit the inference.
Common defects and patterns
”Missing lifetime specifier”
fn longer(a: &str, b: &str) -> &str { // ERROR
if a.len() > b.len() { a } else { b }
}
The compiler doesn’t know whether the result borrows from a or b. The fix is the explicit lifetime annotation:
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
“Borrowed value does not live long enough”
let r;
{
let s = String::from("hello");
r = &s; // ERROR: s doesn't live long enough
}
println!("{}", r);
The fix is to scope r appropriately or to clone:
let s = String::from("hello");
let r = &s;
println!("{}", r); // OK
“Cannot return reference to local variable”
fn make_string() -> &str { // ERROR
let s = String::from("hello");
&s // would dangle; s dropped at end of fn
}
The fix is to return the owned value:
fn make_string() -> String {
String::from("hello") // owned; transferred to caller
}
Lifetime in a closure
let s = String::from("hello");
let f = || println!("{}", s); // f borrows s
let r = &s; // ERROR: s is borrowed by f
f();
The closure captures by reference; the conventional fix is move:
let s = String::from("hello");
let f = move || println!("{}", s); // f owns s
println!("done"); // s is no longer accessible here
f();
The move admits the closure to take ownership of captured variables.
A note on the conventional discipline
The contemporary Rust lifetime advice:
- Trust elision for the simple cases.
- Add explicit lifetimes when the compiler requires.
- Prefer owned types (
String,Vec<T>,Box<T>) for fields that don’t need to borrow; lifetimes on structs add complexity. - Use
'staticfor “this doesn’t borrow from anywhere short-lived”. - Read the compiler’s lifetime errors — they often suggest the fix.
- Refactor if the lifetime relationship gets tangled — sometimes a different design is clearer.
The combination — elision for the easy cases, explicit annotations for the elaborate ones, the 'static bound for ownership-friendly contexts — is the substance of Rust’s lifetime story. Mastering it admits writing zero-cost code with compile-time safety guarantees that other languages cannot offer.