Lifetime annotations demystified, how Rust tracks reference validity at compile time.
Lifetimes prevent dangling references, pointers to data that has been freed. The Rust compiler uses lifetime annotations to verify that every reference is valid for as long as it is used.
// Problem: dangling references
// fn dangle() -> &String {
// let s = String::from("hello");
// &s // ERROR: s goes out of scope, reference becomes invalid
// }
// Solution: return owned value
fn no_dangle() -> String {
let s = String::from("hello");
s // Ownership transferred to caller
// Lifetime annotation tells compiler how long references are valid
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
let s1 = String::from("long string");
let s2 = "short";
println!("{}", longest(&s1, s2));
// Without lifetime annotations, compiler can't ensure safety:
// fn broken<'a>(s: &str) -> &'a str {
// let local = String::from("local");
// &local // ERROR: local doesn't live long enough
// }
// Lifetime 'a means: reference is valid for at least duration 'a
fn get_first<'a>(data: &'a [&'a str]) -> &'a str {
data[0]
let strs = vec![&"hello", &"world"];
let first = get_first(&strs);
Lifetime annotations do not extend or shorten the actual lifespan of any data. They are a way to tell the compiler about the relationships between the lifetimes of different references, so it can verify correctness at compile time.
Lifetime parameters are declared in angle brackets after the function name, using a tick followed by a lowercase letter: 'a, 'b, etc. They describe how the lifetimes of inputs and outputs relate.
// Single lifetime parameter
fn take_ref<'a>(s: &'a String) -> &'a str {
&s[..]
// Multiple lifetime parameters
fn compare<'a, 'b>(x: &'a str, y: &'b str) -> bool {
x.len() == y.len()
// Lifetime bounds: 'b must live at least as long as 'a
fn subset<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if x.len() < y.len() {
x
} else {
y // y lives longer, can be returned as 'a
}
}
// Struct with lifetime parameters
struct Excerpt<'a> {
part: &'a str,
let text = String::from("hello world");
let excerpt = Excerpt { part: &text[0..5] };
// Method with lifetimes
impl<'a> Excerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
// Complex lifetime relationships
fn complex<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
'b: 'a, // 'b must outlive 'a
{
if x.len() > y.len() {
x
} else {
y
}
}
// Named lifetime for struct references
struct Person {
name: String,
struct Group<'a> {
members: Vec<&'a Person>,
let person = Person { name: String::from("Alice") };
let mut group = Group { members: vec![&person] };
The syntax 'b: 'a means "'b outlives 'a". This lets you return a reference with lifetime 'a from a value that has the longer lifetime 'b. Use where clauses to express these constraints clearly.
Rust has three lifetime elision rules that let the compiler infer lifetimes in common patterns, so you don't need to write them explicitly every time.
// Rule 1: Each input lifetime gets its own lifetime parameter
// These are equivalent:
fn input_ref<'a>(x: &'a str) -> String {
String::from(x)
fn input_ref(x: &str) -> String {
String::from(x)
// Rule 2: If there's one input lifetime, it's assigned to output
// These are equivalent:
fn only_input<'a>(x: &'a str) -> &'a str {
x
fn only_input(x: &str) -> &str {
x
// Rule 3: If there's &self, self's lifetime applies to output
struct Container;
impl Container {
// Implicit: fn first<'a>(&'a self) -> &'a str
fn first(&self) -> &str {
"hello"
}
}
// When elision rules don't apply, explicit annotation required
fn two_inputs<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() {
x
} else {
x // Must choose one lifetime
}
// Compiler can't elide this - which lifetime for output?
// fn ambiguous(x: &str, y: &str) -> &str {
// if true { x } else { y }
// }
// Method with explicit lifetimes
struct Parser<'a> {
input: &'a str,
impl<'a> Parser<'a> {
fn parse(&self) -> &'a str {
self.input
}
fn parse_ref<'b>(&'b self) -> &'b str {
self.input
}
}
In practice, the three elision rules cover the vast majority of cases. You only need explicit lifetime annotations when the compiler cannot determine the relationship between input and output lifetimes, typically when there are multiple reference parameters and a reference return type.
Structs that hold references must declare lifetime parameters. This ensures the struct cannot outlive the data it borrows.
// Simple struct with lifetime
struct Quote<'a> {
text: &'a str,
let text = String::from("Life is short");
let quote = Quote { text: &text };
// Multiple lifetime parameters
struct Relation<'a, 'b> {
person1: &'a String,
person2: &'b String,
let alice = String::from("Alice");
let bob = String::from("Bob");
let rel = Relation { person1: &alice, person2: &bob };
// Lifetime bounds in structs
struct Container<'a, T: 'a> {
items: &'a [T],
let data = vec![1, 2, 3];
let container = Container { items: &data };
// Struct methods with lifetime
struct TextBlock<'a> {
content: &'a str,
impl<'a> TextBlock<'a> {
fn first_word(&self) -> &'a str {
for (i, &item) in self.content.as_bytes().iter().enumerate() {
if item == b' ' {
return &self.content[0..i];
}
}
&self.content[..]
}
let text = String::from("hello world");
let block = TextBlock { content: &text };
let word = block.first_word();
// Struct with generic and lifetime
struct Cache<'a, T> {
data: &'a T,
impl<'a, T: Clone> Cache<'a, T> {
fn get_copy(&self) -> T {
self.data.clone()
}
}
// Multiple references in struct
struct DocumentRef<'a> {
title: &'a str,
content: &'a str,
author: &'a str,
// Nested lifetimes
struct Graph<'a> {
nodes: Vec<&'a str>,
edges: Vec<(&'a str, &'a str)>,
let n1 = "A".to_string();
let n2 = "B".to_string();
let graph = Graph {
nodes: vec![&n1, &n2],
edges: vec![(&n1, &n2)],
};
Use a single lifetime when all references in a struct must come from data with the same scope. Use multiple lifetimes (e.g., 'a, 'b) when references may come from data with different scopes, giving the compiler more flexibility.
The 'static lifetime is special, it means the data lives for the entire duration of the program. String literals and owned types without borrowed references satisfy 'static.
// 'static lifetime: data lives for entire program duration
let s: &'static str = "hello"; // String literals are 'static
// 'static bound: type must only contain 'static references
fn print_static<T: 'static>(val: T) {
// T doesn't contain borrowed references
print_static(String::from("hello"));
print_static(5);
// print_static(&String::from("hello")); // Error: not 'static
// Using 'static with trait objects
trait MyTrait {
fn do_something(&self);
struct MyType;
impl MyTrait for MyType {
fn do_something(&self) {
println!("Doing something");
}
fn get_trait_object() -> Box<dyn MyTrait + 'static> {
Box::new(MyType)
}
// 'static vs non-'static trait objects
fn non_static<'a>(s: &'a str) -> Box<dyn std::fmt::Display + 'a> {
Box::new(s)
// Combining 'static with generics
struct Container<T: 'static> {
data: T,
// Lifetime bounds on generics
fn print_ref<'a, T: 'a + std::fmt::Display>(val: &'a T) {
println!("{}", val);
// 'static bound on function pointers
fn call_fn<F: Fn() + 'static>(f: F) {
f();
// 'a bound on iterators
fn iterate<'a, I>(iter: I)
where
I: Iterator<Item = &'a str>,
{
for item in iter {
println!("{}", item);
}
let strs = vec![&"hello", &"world"];
iterate(strs.iter().cloned());
// Leaked values become 'static
fn leak_to_static<T>(val: T) -> &'static T {
Box::leak(Box::new(val))
let s = leak_to_static(String::from("hello"));
println!("{}", s); // Valid forever
Box::leak intentionally leaks memory and should only be used for data that truly needs to live forever (e.g., global configuration). In most cases, prefer proper lifetime annotations or owned types over 'static bounds.
When functions or structs work with references that have different lifetimes, you may need multiple lifetime parameters and bounds to express the correct relationships.
// Simple multiple lifetimes
fn process<'a, 'b>(x: &'a str, y: &'b str) -> usize {
x.len() + y.len()
// Lifetime ordering: 'b must outlive 'a
fn process_ordered<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
// Three lifetime parameters
struct Triple<'a, 'b, 'c> {
x: &'a str,
y: &'b str,
z: &'c str,
// Relationships between lifetimes
struct StringPair<'a, 'b: 'a> {
first: &'a str,
second: &'b str, // 'b must live at least as long as 'a
// Lifetimes with generics
fn collect_refs<'a, T>(items: &'a [T]) -> Vec<&'a T> {
items.iter().collect()
}
// Lifetime elision in closures (captured)
fn create_closure<'a>(s: &'a str) -> impl Fn() + 'a {
move || println!("{}", s)
// Higher-ranked trait bounds (for all lifetimes)
fn higher_rank<F>(f: F)
where
F: for<'a> Fn(&'a str),
{
f("hello");
f("world");
// Covariance example
fn covariance_example() {
fn print_str<'a>(s: &'a str) {
println!("{}", s);
}
let s: &'static str = "hello";
print_str(s); // 'static is valid where &'a is needed
}
The for<'a> syntax means "for any lifetime 'a". This is used when a closure or function must work with references of any lifetime, not just a specific one. You'll encounter this most often with closures passed as arguments.
Understanding common patterns and mistakes helps you write correct lifetime annotations quickly and avoid frustrating compiler errors.
// Pattern 1: Longest reference
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
let s1 = String::from("long");
let s2 = String::from("short");
let result = longest(&s1, &s2);
println!("{}", result);
// Pattern 2: Return reference from struct
struct Document<'a> {
content: &'a str,
impl<'a> Document<'a> {
fn first_line(&self) -> &'a str {
self.content.lines().next().unwrap()
}
}
// Pitfall 1: Returning reference to local data
// fn pitfall1() -> &str {
// let s = String::from("hello");
// &s // ERROR: s dropped at end of function
// }
// Solution: return owned value
fn solution1() -> String {
String::from("hello")
// Pitfall 2: Unclear lifetime relationships
// fn unclear<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
// if x.len() > y.len() {
// x
// } else {
// y // ERROR: y's lifetime 'b != 'a
// }
// }
// Solution: bound lifetimes
fn clear<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
// Pattern 3: Parser with input lifetime
struct Parser<'a> {
input: &'a str,
position: usize,
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser { input, position: 0 }
}
fn peek(&self) -> &'a str {
&self.input[self.position..]
}
fn take(&mut self, count: usize) -> &'a str {
let result = &self.input[self.position..self.position + count];
self.position += count;
result
}
// Pattern 4: Reference to multiple fields
struct Window<'a> {
title: &'a str,
content: &'a str,
impl<'a> Window<'a> {
fn render(&self) -> String {
format!("{}: {}", self.title, self.content)
}
}
Use 'a for the primary lifetime, 'b, 'c for additional ones. Keep lifetime annotations minimal and clear. Prefer owned types when possible to avoid lifetime complexity. Use 'static only for truly long-lived data.