Lifetimes
Rust lifetime annotations for ensuring reference validity and understanding the borrow checker
You are an expert in Rust lifetimes for annotating reference validity, satisfying the borrow checker, and designing APIs with correct lifetime relationships.
## Key Points
1. Each input reference gets its own lifetime parameter.
2. If there is exactly one input lifetime, it is assigned to all output lifetimes.
3. If one of the inputs is `&self` or `&mut self`, its lifetime is assigned to all outputs.
- Let the compiler infer lifetimes wherever possible — only annotate when required.
- When a struct holds a reference, consider whether it should own the data instead (`String` vs `&str`). Owned data is simpler and avoids lifetime propagation.
- Use `'static` bounds sparingly. Requiring `'static` on a trait bound prevents callers from passing borrowed data.
- Name lifetimes descriptively in complex signatures: `'input`, `'conn`, `'ctx` are clearer than `'a`, `'b`.
- If lifetime annotations are getting unwieldy, it is often a sign that the struct should own its data.
- **Returning a reference to local data**: You cannot return a reference to a value created inside the function — it would be a dangling reference. Return an owned type instead.
- **Lifetime annotation does not extend a borrow**: `'a` does not make data live longer. It constrains the compiler's understanding of existing lifetimes.
- **`'static` does not mean "lives forever on the heap"**: It means the value contains no non-static references. `String` is `'static` because it owns its data. `&str` from a local variable is not.
- **Covariance confusion**: `&'a T` is covariant in `'a` (a longer lifetime can be used where a shorter one is expected), but `&'a mut T` is invariant in `'a`. This matters in advanced generic code.
## Quick Example
```rust
// This function returns a reference that lives as long as the shorter of 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
```
```rust
// Elided (common)
fn first_word(s: &str) -> &str { /* ... */ }
// Fully annotated equivalent
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
```skilldb get rust-skills/LifetimesFull skill: 181 linesLifetimes — Rust Programming
You are an expert in Rust lifetimes for annotating reference validity, satisfying the borrow checker, and designing APIs with correct lifetime relationships.
Core Philosophy
Lifetimes are not a feature you use -- they are a property of your code that the compiler verifies. Every reference already has a lifetime; annotations simply make the relationships explicit when the compiler cannot infer them. Understanding this distinction is essential: you are not assigning lifetimes, you are describing constraints that already exist.
The borrow checker's lifetime analysis is Rust's substitute for a garbage collector. Where a GC tracks reachability at runtime, lifetimes prove at compile time that no reference outlives its data. The cost is occasionally writing 'a annotations; the benefit is zero runtime overhead, no GC pauses, and a guarantee that use-after-free is impossible. When lifetime annotations feel burdensome, it usually means the data ownership model needs rethinking -- not that the compiler is being pedantic.
The elision rules handle the vast majority of cases automatically, which means explicit lifetimes should be rare in well-designed APIs. When you find yourself threading 'a through five layers of structs, that is a signal to step back and ask whether the struct should own its data instead of borrowing it. Lifetimes are cheapest when they stay local -- a function that borrows and returns within a small scope -- and most expensive when they propagate outward through struct fields.
Anti-Patterns
- Adding lifetime parameters to avoid cloning without measuring: Borrowing instead of owning saves an allocation, but if it forces a lifetime parameter onto a struct that propagates through your entire codebase, the complexity cost far exceeds the allocation cost. Profile before optimizing with lifetimes.
- Returning references to local data: Attempting to return
&strfrom aStringcreated inside the function is a dangling reference. The compiler will reject it, but the instinct to "just return a reference" leads to confusing errors. Return owned types from functions that create data. - Using
'staticas a band-aid: When the borrow checker complains, slapping'staticon a bound or leaking withBox::leakmakes the error disappear but creates data that lives forever. This is almost always wrong in application code and leads to memory leaks. - Confusing lifetime annotations with lifetime control: Writing
'aon a function signature does not extend how long data lives. It constrains what the compiler will accept. Misunderstanding this leads to futile attempts to "fix" lifetime errors by adding more annotations rather than restructuring ownership. - Deeply nested lifetime parameterization: A type like
Foo<'a, 'b, 'c>with three lifetime parameters is almost always a design smell. Simplify by having the struct own its data, or by reducing the number of independent borrow scopes.
Overview
Lifetimes are Rust's way of tracking how long references are valid. Most lifetimes are inferred automatically by the compiler through lifetime elision rules. Explicit lifetime annotations are needed when the compiler cannot determine the relationship between input and output references. Lifetimes are a compile-time concept and have zero runtime cost.
Core Concepts
Lifetime Annotations
Lifetime annotations do not change how long data lives — they describe the relationships between lifetimes of references so the compiler can verify safety:
// This function returns a reference that lives as long as the shorter of 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Lifetime Elision Rules
The compiler applies three rules to infer lifetimes, eliminating most explicit annotations:
- Each input reference gets its own lifetime parameter.
- If there is exactly one input lifetime, it is assigned to all output lifetimes.
- If one of the inputs is
&selfor&mut self, its lifetime is assigned to all outputs.
// Elided (common)
fn first_word(s: &str) -> &str { /* ... */ }
// Fully annotated equivalent
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
The 'static Lifetime
'static means the reference is valid for the entire program duration. String literals are 'static:
let s: &'static str = "I live forever";
'static as a trait bound means the type contains no non-static references (it owns all its data or holds only 'static references):
fn spawn_task(task: impl FnOnce() + Send + 'static) {
std::thread::spawn(task);
}
Implementation Patterns
Lifetimes in Structs
A struct that holds references must annotate their lifetimes:
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn new(text: &'a str) -> Self {
Excerpt { text }
}
// Elision rule 3: &self lifetime applied to output
fn summary(&self) -> &str {
if self.text.len() > 50 {
&self.text[..50]
} else {
self.text
}
}
}
Multiple Lifetimes
Use distinct lifetime parameters when inputs have independent lifetimes:
fn select<'a, 'b>(condition: bool, a: &'a str, b: &'b str) -> &'a str
where
'b: 'a, // 'b outlives 'a
{
if condition { a } else { b }
}
Lifetime Bounds on Generics
fn print_ref<'a, T>(t: &'a T)
where
T: std::fmt::Debug + 'a,
{
println!("{t:?}");
}
Structs with Mixed Owned and Borrowed Data
struct Parser<'input> {
source: &'input str,
position: usize,
errors: Vec<String>, // owned, no lifetime needed
}
impl<'input> Parser<'input> {
fn next_token(&mut self) -> Option<&'input str> {
// returns a slice of the original input
if self.position >= self.source.len() {
return None;
}
let start = self.position;
while self.position < self.source.len()
&& !self.source.as_bytes()[self.position].is_ascii_whitespace()
{
self.position += 1;
}
self.position += 1; // skip whitespace
Some(&self.source[start..self.position - 1])
}
}
Higher-Ranked Trait Bounds (HRTBs)
For closures that accept references with any lifetime:
fn apply_to_ref<F>(f: F, value: &str) -> String
where
F: for<'a> Fn(&'a str) -> String,
{
f(value)
}
Best Practices
- Let the compiler infer lifetimes wherever possible — only annotate when required.
- When a struct holds a reference, consider whether it should own the data instead (
Stringvs&str). Owned data is simpler and avoids lifetime propagation. - Use
'staticbounds sparingly. Requiring'staticon a trait bound prevents callers from passing borrowed data. - Name lifetimes descriptively in complex signatures:
'input,'conn,'ctxare clearer than'a,'b. - If lifetime annotations are getting unwieldy, it is often a sign that the struct should own its data.
Common Pitfalls
- Returning a reference to local data: You cannot return a reference to a value created inside the function — it would be a dangling reference. Return an owned type instead.
- Lifetime annotation does not extend a borrow:
'adoes not make data live longer. It constrains the compiler's understanding of existing lifetimes. - Struct lifetime infects all users: A
struct Foo<'a>forces every holder ofFooto also track'a. This can cascade through an entire codebase. Prefer owned data unless borrowing is essential for performance. 'staticdoes not mean "lives forever on the heap": It means the value contains no non-static references.Stringis'staticbecause it owns its data.&strfrom a local variable is not.- Covariance confusion:
&'a Tis covariant in'a(a longer lifetime can be used where a shorter one is expected), but&'a mut Tis invariant in'a. This matters in advanced generic code.
Install this skill directly: skilldb add rust-skills
Related Skills
Async Rust
Async Rust programming with async/await, Tokio runtime, futures, and concurrent task patterns
Cargo Workspace
Cargo workspaces, project structure, dependency management, and multi-crate Rust project organization
Error Handling
Rust error handling with Result, Option, the ? operator, and ecosystem crates anyhow and thiserror
Ownership Borrowing
Rust ownership, borrowing, and move semantics for writing memory-safe code without a garbage collector
Pattern Matching
Rust pattern matching with match, if let, while let, destructuring, and advanced match patterns
Smart Pointers
Rust smart pointers Box, Rc, Arc, RefCell, and their combinations for heap allocation and shared ownership