Rust Lifetimes Explained Simply with Examples
Learn Rust lifetimes from scratch with clear visual examples, common patterns, and practical rules that make lifetime annotations easy to understand.
What you'll learn
- ✓What lifetimes are and why Rust needs them
- ✓How to read and write lifetime annotations
- ✓The three lifetime elision rules that save you typing
- ✓Common lifetime patterns in functions and structs
Prerequisites
- •Basic Rust syntax — see /blog/rust-variables-and-types
- •Ownership and borrowing — see /blog/rust-ownership-basics
Every value in Rust has an owner, and every reference has a lifetime. A lifetime is the span of code during which a reference is guaranteed to be valid. Most of the time, lifetimes are invisible because the compiler figures them out for you. But sometimes you need to give the compiler a hint, and that is where lifetime annotations come in.
This guide walks through lifetimes step by step with examples that build on each other. By the end, you will be comfortable reading and writing lifetime annotations in everyday Rust code.
Why Lifetimes Exist
Consider this broken code:
fn main() {
let r;
{
let x = 5;
r = &x; // x is about to be dropped
}
// r now points to freed memory — Rust refuses to compile
println!("{}", r);
}
The variable x lives only inside the inner block. Once that block ends, x is dropped, and r would be a dangling reference. Rust prevents this at compile time using lifetimes. The compiler tracks how long each reference lives and rejects code where a reference outlives its data.
You never need a garbage collector because the compiler does all this analysis before your program runs.
Lifetime Annotations Are Labels, Not Instructions
A common misconception is that lifetime annotations change how long something lives. They do not. They are labels that tell the compiler about relationships between references. Think of them as sticky notes that say “these two references must live at least as long as each other.”
The syntax uses an apostrophe followed by a short name:
&'a str // a reference to a str with lifetime 'a
&'a mut i32 // a mutable reference to an i32 with lifetime 'a
The name 'a is arbitrary. You could use 'input or 'src, but single letters are conventional.
Your First Lifetime Annotation
Here is a function that returns the longer of two string slices:
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!("Longest: {}", result);
}
// Using result here would fail because s2 is dropped
}
The annotation <'a> says: “the returned reference will live at least as long as the shorter of the two input lifetimes.” The compiler uses this information to check that result is never used after either input has been dropped.
Without the annotation, the compiler cannot know whether the return value comes from x or y, so it cannot verify safety. The annotation resolves that ambiguity.
When You Do Not Need Annotations
Rust has three lifetime elision rules that let you skip annotations in common cases. The compiler applies them in order:
Rule 1: Each reference parameter gets its own lifetime. A function fn foo(x: &str, y: &str) is treated as fn foo<'a, 'b>(x: &'a str, y: &'b str).
Rule 2: If there is exactly one input lifetime, it is assigned to all output lifetimes. So fn first_word(s: &str) -> &str becomes fn first_word<'a>(s: &'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.
If after applying all three rules the compiler still cannot determine output lifetimes, it asks you to annotate.
// No annotation needed — Rule 2 applies
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
s
}
Lifetimes in Structs
When a struct holds a reference, you must annotate the lifetime so the compiler knows the struct cannot outlive the data it borrows:
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn level(&self) -> i32 {
3
}
// Rule 3 applies — return lifetime tied to &self
fn announce(&self, prefix: &str) -> &str {
println!("Attention: {}", prefix);
self.text
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel
.split('.')
.next()
.expect("Could not find a period");
let excerpt = Excerpt {
text: first_sentence,
};
println!("Excerpt: {}", excerpt.text);
println!("Level: {}", excerpt.level());
}
The Excerpt struct borrows from novel. As long as novel stays alive, the excerpt is valid. If you tried to drop novel before excerpt, the compiler would stop you.
Multiple Lifetimes
Sometimes inputs have genuinely different lifetimes and you need to express that:
fn first_or_default<'a, 'b>(input: &'a str, default: &'b str) -> &'a str {
if input.is_empty() {
// This would fail — we promised to return something with lifetime 'a
// but default has lifetime 'b
// default // ERROR
panic!("input was empty");
} else {
input
}
}
If you want to return either one, both lifetimes must unify. You can add a bound 'b: 'a meaning 'b lives at least as long as 'a:
fn first_or_default<'a, 'b: 'a>(input: &'a str, default: &'b str) -> &'a str {
if input.is_empty() {
default // now ok — 'b outlives 'a
} else {
input
}
}
fn main() {
let default = "N/A";
let result;
{
let input = String::from("hello");
result = first_or_default(&input, default);
println!("{}", result);
}
}
The ‘static Lifetime
The 'static lifetime means the reference is valid for the entire program. String literals are the most common example:
let s: &'static str = "I live forever";
The string data is embedded in the binary, so it genuinely lives as long as the program runs.
You will also see 'static in trait bounds:
fn spawn_task(name: String) {
std::thread::spawn(move || {
println!("Task: {}", name);
});
}
The spawn function requires 'static because the new thread might outlive the caller. Using move transfers ownership, satisfying the bound without needing a 'static reference.
Avoid using 'static as a quick fix for lifetime errors. It usually means you are hiding a design issue. Ask yourself: does this data really need to live forever?
Lifetime Annotations in Practice
Here is a more realistic example parsing configuration values:
#[derive(Debug)]
struct Config<'a> {
host: &'a str,
port: u16,
}
fn parse_config(input: &str) -> Config<'_> {
let mut host = "localhost";
let mut port = 8080;
for line in input.lines() {
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() == 2 {
let key = parts[0].trim();
let value = parts[1].trim();
match key {
"host" => host = value,
"port" => port = value.parse().unwrap_or(8080),
_ => {}
}
}
}
Config { host, port }
}
fn main() {
let raw = String::from("host = example.com\nport = 3000");
let config = parse_config(&raw);
println!("{:?}", config);
// config borrows from raw — both must stay alive here
}
The '_ in the return type tells the compiler to infer the lifetime. It is equivalent to writing Config<'a> with an explicit annotation. Use '_ when the lifetime is unambiguous and you want to keep the code clean.
Common Mistakes and How to Fix Them
Returning a reference to a local variable:
// WRONG — data does not live long enough
fn bad() -> &str {
let s = String::from("hello");
&s // s is dropped at the end of the function
}
// FIX — return an owned value instead
fn good() -> String {
String::from("hello")
}
Storing references with mismatched lifetimes:
struct Cache<'a> {
data: Vec<&'a str>,
}
fn build_cache<'a>(items: &'a [String]) -> Cache<'a> {
let data: Vec<&str> = items.iter().map(|s| s.as_str()).collect();
Cache { data }
}
The cache borrows from the slice, so the slice must outlive the cache. If you need the cache to own its data independently, use Vec<String> instead of Vec<&str>.
A Mental Model for Lifetimes
Think of lifetimes as scopes drawn on your code. Each opening brace starts a scope, each closing brace ends one. A reference is valid within its scope. Lifetime annotations tell the compiler that two scopes overlap in a specific way.
When the compiler complains about lifetimes, ask these questions:
- Which reference is the compiler worried about?
- What data does it point to?
- When does that data get dropped?
- Is the reference used after the drop?
Nine times out of ten, the fix is either to reorder your code so the data lives long enough, or to switch from borrowing to owning the data.
When to Use Owned Types Instead
Lifetime annotations add complexity. For many programs, especially application code rather than library code, owning the data with String instead of &str is simpler and carries negligible performance cost. Use references and lifetimes when:
- You are writing a library that should not force allocations on callers.
- You are working with large data and copies would be expensive.
- You need to share read-only access without cloning.
For everything else, consider owned types first and add lifetimes only when profiling shows they matter.
Wrapping Up
Lifetimes are not a punishment — they are a tool that helps Rust guarantee memory safety without a garbage collector. The key ideas are simple:
- Every reference has a lifetime that the compiler tracks.
- Annotations are labels that describe relationships between lifetimes.
- Elision rules handle the common cases automatically.
- Structs that hold references need lifetime parameters.
- The
'staticlifetime means “lives for the whole program.” - When in doubt, use owned types and add borrowing later.
Once you internalize these rules, lifetime errors stop being scary and start being helpful hints about where your data flow needs clarification.
Related articles
- Rust Rust Lifetimes Deep Dive
Lifetimes are how Rust tracks how long references live. This deep dive covers annotations, elision rules, generic lifetimes, and the common error patterns.
- Rust Rust Ownership and Borrowing Explained
A practical introduction to Rust's ownership and borrowing rules: moves, references, mutability, and the mental model that makes the borrow checker stop fighting you.
- Rust Rust Async Programming with Tokio: Getting Started
Learn async Rust with Tokio from scratch. Covers async/await syntax, spawning tasks, channels, TCP servers, and common pitfalls with working examples.
- Rust Rust Concurrency: Channels, Mutex, and Arc Patterns
Learn Rust concurrency with channels, Mutex, Arc, and thread-safe patterns. Covers mpsc, shared state, thread pools, and real-world examples.