Lifetimes
Overview
Lifetimes are Rust’s way of ensuring references are always valid. Every reference in Rust has a lifetime—the scope during which that reference is valid. Most of the time, lifetimes are implicit and inferred, just like types.
flowchart LR
subgraph Problem["The Problem"]
A[Reference] --> B[Data]
B --> C[Data Dropped]
A --> D["❌ Dangling Reference!"]
end
subgraph Solution["Rust's Solution"]
E[Reference with Lifetime 'a] --> F[Data]
G["Compiler checks 'a"]
G --> H["✓ Reference always valid"]
end
Key insight: Lifetimes don’t change how long data lives—they describe relationships between references that the compiler can verify.
When Do You Need Lifetime Annotations?
| Situation | Annotation Needed? |
|---|---|
| Function with one input reference | No (elided) |
| Function with multiple input references returning a reference | Yes |
| Struct holding a reference | Yes |
Methods returning &self data |
No (elided) |
| Generic functions with references | Usually yes |
flowchart TD
A{Does function return a reference?} -->|No| B[No annotation needed]
A -->|Yes| C{How many input references?}
C -->|One| D[Annotation elided]
C -->|Multiple| E{Is one &self?}
E -->|Yes| F[Annotation elided]
E -->|No| G["Annotation required"]
style G fill:#FFB6C1
Understanding Lifetimes Visually
A lifetime is simply the scope where a reference is valid:
fn main() {
let r; // ---------+-- 'a starts
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | | 'b is shorter than 'a
} // -+ | x dropped here
// |
// println!("{}", r); // Error! | r would be dangling
} // ---------+
gantt
title Lifetime Visualization
dateFormat X
axisFormat %s
section Variables
x exists :a, 0, 3
r exists :b, 0, 5
section References
r = &x valid :crit, c, 2, 3
r dangling :done, d, 3, 5
The borrow checker ensures no reference outlives the data it refers to.
Lifetime Annotation Syntax
Lifetime annotations describe relationships—they don’t change how long values live.
&i32 // A reference (lifetime inferred)
&'a i32 // A reference with explicit lifetime 'a
&'a mut i32 // A mutable reference with lifetime 'a
Reading Lifetime Annotations
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
// ^^^ ^^ ^^ ^^
// │ │ │ │
// │ └───────────┴────────────┘
// │ All these share lifetime 'a
// │
// └── Declare lifetime parameter
Translation: “The returned reference will be valid as long as both input references are valid.”
The longest Function: A Deep Dive
The Problem
// Won't compile! Which input does the return come from?
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
The compiler doesn’t know if the return value comes from x or y, so it can’t verify the lifetime.
The Solution
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
flowchart TD
subgraph Inputs
X["x: &'a str"]
Y["y: &'a str"]
end
subgraph Function["longest<'a>"]
C{x.len > y.len?}
end
subgraph Output
R["return: &'a str"]
end
X --> C
Y --> C
C -->|Yes| R
C -->|No| R
style X fill:#90EE90
style Y fill:#90EE90
style R fill:#90EE90
How It Works in Practice
fn main() {
let s1 = String::from("long"); // ----+-- s1's scope
{ // |
let s2 = String::from("longer"); // -+--|-- s2's scope
// | |
let result = longest(&s1, &s2); // | |
println!("Longest: {}", result); // | | ✓ Both valid here
} // -+ | s2 dropped
// |
// Can't use result here—s2 is gone! // |
} // ----+ s1 dropped
The lifetime 'a becomes the overlap of both input lifetimes—the shorter of the two.
Lifetime Elision Rules
The compiler automatically infers lifetimes using three rules:
Rule 1: Each Input Gets Its Own Lifetime
fn foo(x: &str, y: &str)
// Compiler sees:
fn foo<'a, 'b>(x: &'a str, y: &'b str)
Rule 2: Single Input → Output Gets Same Lifetime
fn foo(x: &str) -> &str
// Compiler sees:
fn foo<'a>(x: &'a str) -> &'a str
Rule 3: &self Input → Output Gets self’s Lifetime
impl MyStruct {
fn foo(&self, x: &str) -> &str
// Compiler sees:
fn foo<'a, 'b>(&'a self, x: &'b str) -> &'a str
}
flowchart TD
A[Input References] --> B{Apply Rule 1}
B --> C[Each input gets own lifetime]
C --> D{Only one input lifetime?}
D -->|Yes| E[Apply Rule 2: output = input]
D -->|No| F{Is one input &self?}
F -->|Yes| G[Apply Rule 3: output = self]
F -->|No| H["❌ Can't infer - need annotation"]
style H fill:#FFB6C1
Structs with References
When a struct holds a reference, it needs a lifetime parameter:
struct Excerpt<'a> {
part: &'a str,
}
Why? The struct can’t outlive the data it references.
flowchart LR
subgraph Stack
E["Excerpt<'a>"]
end
subgraph "String Data"
S["novel: String"]
P["'Call me Ishmael'"]
end
E -->|"part: &'a str"| P
S --> P
Complete Example
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
// ↑ novel owns the String data
let first_sentence = novel.split('.').next().unwrap();
// ↑ first_sentence borrows from novel
let excerpt = Excerpt { part: first_sentence };
// ↑ excerpt.part borrows from novel (transitively)
println!("Excerpt: {}", excerpt.part);
}
// novel dropped here - excerpt can't outlive novel
Methods on Structs with Lifetimes
impl<'a> Excerpt<'a> {
// No lifetime annotation needed - returns owned data
fn level(&self) -> i32 {
3
}
// Rule 3 applies - output tied to &self
fn announce(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part // Returns data from self
}
}
The 'static Lifetime
'static means the reference is valid for the entire program duration.
// String literals are 'static - baked into binary
let s: &'static str = "I live forever!";
When 'static Appears
| Source | Example |
|---|---|
| String literals | "hello" |
| Constants | const X: &str = "hi" |
| Leaked memory | Box::leak(...) |
| Global statics | static S: &str = "..." |
Don’t use 'static to “fix” lifetime errors! It usually means your design needs rethinking.
// ❌ WRONG - Don't do this
fn bad<'a>(x: &'a str) -> &'static str {
// ...
}
// ✓ RIGHT - Return owned data instead
fn good(x: &str) -> String {
x.to_string()
}
Multiple Lifetime Parameters
When lifetimes are truly independent:
fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
println!("y: {}", y); // Use y but don't return it
x // Only return x - only 'a matters for output
}
Lifetime Bounds
Express that one lifetime must outlive another:
// 'b: 'a means 'b lives at least as long as 'a
fn foo<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > 0 { x } else { y } // Can return either now
}
gantt
title Lifetime Bounds: 'b must outlive 'a
dateFormat X
axisFormat %s
section Lifetimes
'b (longer) :a, 0, 5
'a (shorter) :b, 1, 4
Lifetimes with Generics
Combine type parameters and lifetime parameters:
use std::fmt::Display;
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement: {}", ann);
if x.len() > y.len() { x } else { y }
}
Common Mistakes and Fixes
Mistake 1: Returning Reference to Local
// ❌ WON'T COMPILE
fn create_string() -> &String {
let s = String::from("hello");
&s // s is dropped at end of function!
}
// ✓ FIX: Return owned value
fn create_string() -> String {
String::from("hello")
}
Mistake 2: Struct Outliving Data
// ❌ WON'T COMPILE
fn create_excerpt() -> Excerpt {
let s = String::from("hello");
Excerpt { part: &s } // s dropped, excerpt would dangle!
}
// ✓ FIX: Store owned data
struct OwnedExcerpt {
part: String, // Own the data
}
fn create_excerpt() -> OwnedExcerpt {
OwnedExcerpt { part: String::from("hello") }
}
Mistake 3: Conflicting Lifetimes
// ❌ WON'T COMPILE
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() { x } else { y } // Can't return y with lifetime 'b!
}
// ✓ FIX: Use same lifetime
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Mental Model
Think of lifetimes as contracts:
flowchart TD
A["Function signature with lifetimes"] --> B["Contract with callers"]
B --> C["'I promise the returned reference<br/>will be valid as long as<br/>the inputs are valid'"]
D["Struct with lifetime"] --> E["Contract with users"]
E --> F["'I promise not to outlive<br/>the data I reference'"]
Summary
| Concept | Syntax | When Needed |
|---|---|---|
| Lifetime parameter | 'a |
Declare in <> |
| Reference with lifetime | &'a str |
When compiler can’t infer |
| Static lifetime | 'static |
Program-duration references |
| Lifetime bound | 'b: 'a |
When one must outlive another |
| Struct lifetime | struct Foo<'a> |
Struct holds references |
Key Takeaways
- Lifetimes prevent dangling references - They don’t change when data is dropped
- Most lifetimes are inferred - Elision rules handle common cases
- Annotations describe relationships - They help the compiler verify safety
- When in doubt, own the data -
Stringinstead of&str, etc. 'staticis rarely the answer - Rethink your design instead
Exercises
- Add lifetime annotations to make this compile:
fn first_word(s: &str) -> &str { &s[..s.find(' ').unwrap_or(s.len())] } - Explain why this doesn’t compile:
struct Holder<'a> { value: &'a str, } fn create() -> Holder { let s = String::from("hello"); Holder { value: &s } } - Fix this function:
fn longest_or_default<'a>(x: &'a str, y: &'a str, default: &str) -> &'a str { if x.len() > 0 { x } else if y.len() > 0 { y } else { default } }
See Also
Next Steps
Learn about Structs to create custom data types.