TL;DR
- Lifetimes ensure references are always valid — no dangling pointers, ever.
- The compiler usually infers lifetimes (elision rules). You annotate when it can't.
'adoesn't change how long data lives — it describes existing relationships.'static= the data lives for the entire program duration.
Step 1: What Lifetimes Solve
Lifetimes are Rust's answer to use-after-free bugs — the #1 cause of memory safety vulnerabilities in C/C++ (responsible for ~70% of Chrome and Windows security bugs). Instead of garbage collection (runtime cost) or manual memory management (human error), Rust tracks at compile time how long each reference is valid. When the compiler can't figure it out automatically, you annotate lifetimes to tell it the relationship between references. The result: zero-cost memory safety with no runtime overhead.
// ❌ This is what lifetimes prevent — dangling reference
fn dangling() -> &String {
let s = String::from("hello");
&s // s is dropped here — reference would be invalid!
}
// ✅ Return owned data instead
fn not_dangling() -> String {
String::from("hello") // Ownership transferred to caller
}
Every reference in Rust has a lifetime — the scope for which that reference is valid. The borrow checker ensures no reference outlives its data.
Step 2: Lifetime Annotations
Lifetime annotations ('a, 'b) are the syntax for telling the compiler "these references are related — the output lives at least as long as this input." They don't change how long data lives; they describe existing relationships so the compiler can verify correctness. You only need them when the compiler can't infer the relationship (multiple references in, one reference out — which input does the output borrow from?). Think of them as documentation that the compiler enforces.
Annotations don't change lifetimes — they describe the relationship between reference lifetimes:
// This function says: "the returned reference lives as long as BOTH inputs"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(&string1, &string2);
println!("{result}"); // ✅ Both references still valid here
}
// println!("{result}"); // ❌ string2 is dropped — result might point to it
}
Reading the Syntax
fn example<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
// ^^ ^^ ^^ ^^ ^^
// | | | | |
// | | input 1 input 2 output shares lifetime with x
// | lifetime parameter 2
// lifetime parameter 1
Step 3: Lifetime Elision Rules
Elision rules are the compiler's shortcuts for inferring lifetimes in common patterns, so you don't annotate every function. They were added because early Rust required explicit lifetimes everywhere, which was verbose and scared newcomers. The three rules handle ~90% of cases: one input reference means the output borrows from it; methods borrow from &self. When elision can't determine the relationship (multiple input references), you must annotate explicitly. Understanding elision explains why some functions need annotations and others don't.
The compiler can infer lifetimes in common cases. These are the rules (applied in order):
Rule 1: Each reference parameter gets its own lifetime
fn foo(x: &str) → fn foo<'a>(x: &'a str)
fn foo(x: &str, y: &str) → fn foo<'a, 'b>(x: &'a str, y: &'b str)
Rule 2: If there's exactly one input lifetime, it's assigned to all outputs
fn first_word(s: &str) -> &str
// Becomes: fn first_word<'a>(s: &'a str) -> &'a str
// No annotation needed!
Rule 3: If one of the inputs is &self or &mut self, that lifetime is assigned to outputs
impl MyStruct {
fn get_name(&self) -> &str {
// Return lifetime = self's lifetime
// No annotation needed!
&self.name
}
}
When You MUST Annotate
// Multiple input references + output reference
// Compiler can't guess which input the output relates to
fn longest(x: &str, y: &str) -> &str { ... }
// ❌ Error! Needs lifetime annotations
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }
// ✅ Tells compiler: output lives as long as the shorter of x and y
Step 4: Lifetimes in Structs
Struct lifetime parameters are needed when a struct holds borrowed data (references) instead of owned data. This pattern exists because sometimes you don't want to clone/copy data into a struct — you want to borrow it for performance. The lifetime annotation tells the compiler "this struct can't outlive the data it borrows from," preventing dangling references. It's common in parsers (borrow from input string), iterators (borrow from collection), and zero-copy deserialization.
If a struct holds a reference, it needs a lifetime parameter:
// This struct borrows data — it can't outlive what it borrows
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn new(text: &'a str) -> Self {
Excerpt { text }
}
// Method can return references tied to self's lifetime
fn first_word(&self) -> &str {
self.text.split_whitespace().next().unwrap_or("")
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let excerpt = Excerpt::new(&novel);
println!("{}", excerpt.text); // ✅ novel is still alive
}
// novel dropped here → excerpt would be invalid (but it's already dropped too)
When to Use References in Structs vs Owned Data
| Scenario | Use | Why |
|---|---|---|
| Parsing/views into existing data | &'a str |
Avoid copying large data |
| Config/static data | &'static str |
Known at compile time |
| General-purpose struct | String (owned) |
Simpler, no lifetime complexity |
| Performance-critical, short-lived | &'a T |
Zero-copy |
Step 5: 'static Lifetime
'static is the longest possible lifetime — data that lives for the entire program. String literals are 'static (compiled into the binary), and 'static bounds on trait objects/generics mean "this value owns its data or contains no short-lived references." It's commonly misunderstood as "lives forever" when it actually means "can live as long as needed." You encounter it with tokio::spawn (spawned tasks need 'static because they might outlive the spawner) and Box<dyn Error>.
'static means the data lives for the entire program:
// String literals are 'static — embedded in the binary
let s: &'static str = "hello, world";
// Constants are 'static
static CONFIG: &str = "production";
// Owned types satisfy 'static (they own their data, no borrows)
// This is why tokio::spawn requires T: Send + 'static
let owned = String::from("hello");
tokio::spawn(async move {
println!("{owned}"); // ✅ String is 'static (owns its data)
});
'static Doesn't Mean "Lives Forever"
A common misconception. T: 'static means "T doesn't contain any non-static references" — it CAN be dropped. It just doesn't borrow from short-lived data.
// String is 'static (owns its data) but can absolutely be dropped
let s = String::from("hello");
drop(s); // ← perfectly fine
Step 6: Advanced Patterns
Advanced lifetime patterns handle real-world complexity: multiple lifetimes when a function borrows from two different sources with different validity periods, lifetime bounds on generics (T: 'a means T's references must outlive 'a), and Higher-Ranked Trait Bounds (HRTBs) for closures that work with any lifetime. These patterns appear in library code, trait implementations, and anywhere you need maximum flexibility while maintaining the compiler's safety guarantees.
Multiple Lifetimes
// Different lifetimes when inputs have different scopes
struct Parser<'input, 'config> {
input: &'input str,
config: &'config Config,
}
// Output tied to specific input
impl<'input, 'config> Parser<'input, 'config> {
fn next_token(&self) -> &'input str {
// Returns slice of input, not config
&self.input[0..5]
}
}
Lifetime Bounds
// T must outlive 'a
fn print_ref<'a, T: 'a + std::fmt::Display>(data: &'a T) {
println!("{data}");
}
// Common in trait objects
fn get_display<'a>(items: &'a [Box<dyn Display + 'a>]) -> &'a dyn Display {
&*items[0]
}
Higher-Ranked Trait Bounds (HRTB)
// "for any lifetime 'a, F takes a &'a str"
fn apply<F>(f: F, data: &str)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
let result = f(data);
println!("{result}");
}
Step 7: Common Lifetime Errors & Fixes
Lifetime errors are Rust's most confusing compiler messages for newcomers, but they follow predictable patterns. "Does not live long enough" means you're trying to use a reference after its source is dropped. "Cannot return reference to local variable" means you're creating data in a function and trying to return a reference to it (return owned data instead). This table maps common errors to their causes and fixes, turning cryptic compiler messages into actionable solutions.
| Error | Cause | Fix |
|---|---|---|
missing lifetime specifier |
Multiple inputs, compiler can't infer | Add <'a> annotations |
borrowed value does not live long enough |
Reference outlives data | Extend data's scope or use owned type |
cannot return reference to local variable |
Returning ref to stack data | Return owned type or take reference as input |
lifetime may not live long enough |
Return lifetime doesn't match input | Align lifetimes in signature |
Quick Fix Decision Tree
Error involves lifetimes?
├── Returning a reference?
│ ├── From local data → Return owned type (String, Vec, etc.)
│ └── From input → Add lifetime tying output to input
├── Struct holds a reference?
│ ├── Short-lived usage → Add lifetime parameter to struct
│ └── Long-lived struct → Use owned types (String instead of &str)
└── Trait object needs lifetime?
└── Add + 'a or + 'static bound
Interview Questions
-
What are lifetimes in Rust?
- Annotations that describe how long references are valid. They let the compiler verify no reference outlives its data, preventing dangling pointers at compile time.
-
What's the difference between
&strandString?&stris a borrowed reference (has a lifetime, doesn't own data).Stringis owned (no lifetime needed, allocated on heap, can be mutated). UseStringfor owned data,&strfor read-only views.
-
What does
'staticmean?- The data doesn't contain non-static borrows. It CAN be dropped — it just doesn't reference short-lived data. All owned types (String, Vec) satisfy
'static.
- The data doesn't contain non-static borrows. It CAN be dropped — it just doesn't reference short-lived data. All owned types (String, Vec) satisfy
-
When can you avoid lifetime annotations?
- When elision rules apply: single input reference (Rule 2), or methods with
&self(Rule 3). You only annotate when there are multiple input references and the compiler can't infer which one the output relates to.
- When elision rules apply: single input reference (Rule 2), or methods with