09. Lifetimes
📋 Jump to Takeaways🎁 The compiler asks: "prove this reference will still be valid when you use it." Your answer is a single character — 'a. That's how Rust guarantees you'll never dereference a dangling pointer without a garbage collector.
Why Lifetimes Exist
Lifetimes prevent dangling references — pointers to memory that has already been freed. In C or C++, this is a use-after-free bug that leads to crashes or security vulnerabilities. Rust catches it at compile time.
fn main() {
let r;
{
let x = 5;
r = &x;
} // x is dropped here
// println!("{}", r); // ERROR: `x` does not live long enough
}The compiler sees that x lives only inside the inner block, but r tries to use it outside. Rust refuses to compile this. No runtime check — just static analysis using lifetimes.
Lifetime Annotations on Functions
When a function takes multiple references and returns a reference, the compiler needs help. It can't tell which input the output borrows from.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("hi");
result = longest(s1.as_str(), s2.as_str());
println!("{}", result); // Works here
}
// println!("{}", result); // ERROR: result's lifetime is tied to s2, which is dropped here
}The annotation 'a says: "the returned reference lives at least as long as the shorter of x and y." You're not changing how long things live — you're telling the compiler how the lifetimes relate.
When the Compiler Can't Figure It Out
With one reference parameter, the compiler knows the output must borrow from it. But with two or more reference parameters, ambiguity appears.
// This won't compile — which input does the return borrow from?
// fn first_word(s1: &str, s2: &str) -> &str {
// &s1[..1]
// }
// Fix: annotate to show it borrows from s1
fn first_word<'a>(s1: &'a str, _s2: &str) -> &'a str {
&s1[..1]
}
fn main() {
let word = first_word("hello", "world");
println!("{}", word); // "h"
}Notice _s2 doesn't need 'a because the return value doesn't borrow from it. You only annotate the relationships that matter.
Lifetime Elision Rules
You don't always write lifetime annotations. The compiler applies three rules automatically:
Rule 1: Each reference parameter gets its own lifetime. fn foo(x: &str, y: &str) becomes fn foo<'a, 'b>(x: &'a str, y: &'b str).
Rule 2: If there's exactly one input lifetime, it's assigned to all output lifetimes. fn foo(x: &str) -> &str becomes fn foo<'a>(x: &'a str) -> &'a str.
Rule 3: If one of the parameters is &self or &mut self, the lifetime of self is assigned to all output lifetimes.
// No annotations needed — Rule 2 applies
fn first_three(s: &str) -> &str {
&s[..3]
}
// No annotations needed — Rule 3 applies
struct Config {
name: String,
}
impl Config {
fn get_name(&self) -> &str {
&self.name
}
}
fn main() {
let name = first_three("hello");
println!("{}", name); // "hel"
}If the three rules don't fully determine output lifetimes, the compiler asks you to annotate explicitly.
Lifetimes in Structs
When a struct holds a reference, you must annotate it. This tells the compiler: "this struct cannot outlive the data it borrows."
#[derive(Debug)]
struct Excerpt<'a> {
text: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt { text: first_sentence };
println!("{:?}", excerpt);
// Excerpt { text: "Call me Ishmael" }
}If you tried to use excerpt after novel is dropped, the compiler would reject it. The lifetime 'a on the struct enforces this.
// This won't compile:
// fn broken() -> Excerpt {
// let s = String::from("temporary");
// Excerpt { text: &s } // ERROR: s doesn't live long enough
// }The 'static Lifetime
The 'static lifetime means the reference lives for the entire program. String literals are 'static because they're baked into the binary.
let s: &'static str = "I live forever";
fn get_greeting() -> &'static str {
"Hello, world!" // string literals are 'static
}
fn main() {
println!("{}", get_greeting()); // "Hello, world!"
}You'll also see 'static in trait bounds like T: Send + 'static, meaning the type owns all its data (no borrowed references). Don't slap 'static on everything to "fix" lifetime errors — it usually means you should restructure your code instead.
Common Lifetime Errors and Fixes
Error: returning a reference to a local variable.
// BROKEN
// fn make_greeting(name: &str) -> &str {
// let greeting = format!("Hello, {}", name);
// &greeting // ERROR: returns reference to local
// }
// FIX: return an owned String instead
fn make_greeting(name: &str) -> String {
format!("Hello, {}", name)
}
fn main() {
let g = make_greeting("Rust");
println!("{}", g); // "Hello, Rust"
}Error: conflicting lifetimes.
// BROKEN — compiler can't satisfy both lifetimes
// fn pick<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
// if x.len() > 0 { x } else { y } // ERROR: y has lifetime 'b, not 'a
// }
// FIX: use the same lifetime for both
fn pick<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > 0 { x } else { y }
}
fn main() {
let result = pick("hello", "world");
println!("{}", result); // "hello"
}Error: struct outlives borrowed data.
// BROKEN
// fn create_excerpt() -> Excerpt {
// let data = String::from("temp");
// Excerpt { text: &data } // ERROR: data dropped at end of fn
// }
// FIX: ensure the data lives long enough, or store owned data
struct OwnedExcerpt {
text: String,
}
fn create_excerpt() -> OwnedExcerpt {
let data = String::from("temp");
OwnedExcerpt { text: data }
}The pattern is clear: if you can't guarantee the borrowed data outlives the reference, switch to owned data.
Key Takeaways
- Lifetimes prevent dangling references at compile time — no runtime cost
- Annotations like
'adescribe relationships between references, they don't change how long data lives - The compiler applies three elision rules so you rarely write annotations in practice
- Structs holding references need lifetime annotations
'staticmeans "lives for the entire program" — use it sparingly- When lifetime errors appear, consider whether you should return owned data instead
🎁 Your code is safe, but it's all in one giant file. Next up: Modules and Crates — you'll learn how Rust's module system splits code across files and packages, and how to share your work with the world via crates.io.