Rust Lifetimes: A Gentle Intro
A friendly introduction to Rust lifetimes — what they are, when you must annotate them, common function signatures, elision rules, and struct lifetimes.
What you'll learn
- ✓Read lifetime annotations like 'a
- ✓Recognize when the compiler needs them
- ✓Apply the three lifetime elision rules
- ✓Carry references inside structs safely
- ✓Use the static lifetime appropriately
Prerequisites
- •Working Rust setup — see /blog/rust-install-and-first-program
- •Comfort with types — see /blog/rust-variables-and-types
- •Ownership and borrowing — see /blog/rust-ownership-basics
Lifetimes are the part of Rust that intimidates newcomers the most, but they are a smaller idea than they look. A lifetime is the compiler’s way of describing how long a reference is valid. You usually never write them by hand — the compiler infers them. The cases where you do write them follow a small set of patterns.
What a Lifetime Actually Is
Every reference in Rust has a lifetime. It is a region of code in which the borrow is guaranteed to be valid. The borrow checker compares the lifetimes of references to make sure none of them outlive the data they point at.
let r;
{
let x = 5;
r = &x; // r borrows x
} // x dropped here
// println!("{}", r); // would not compile — r outlives x
There is no syntax in this example because the compiler can see everything. Lifetime syntax only shows up when the compiler can’t connect the dots on its own.
When You Need to Annotate
The classic trigger is a function that takes references and returns a reference. Consider returning the longer of two string slices.
fn longest(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
This fails to compile. The compiler asks: does the returned reference borrow from a, from b, or both? Without an answer, callers cannot know how long the result is valid.
We answer with a generic lifetime parameter.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
'a is just a name. We are saying both inputs and the output share a lifetime: the returned reference will be valid as long as both inputs remain valid. The compiler now has enough information to check call sites.
Reading Lifetime Signatures
A few common shapes appear often.
// Borrow from one specific input
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
// Two unrelated lifetimes
fn combine<'a, 'b>(a: &'a str, b: &'b str) -> String { /* owned result */ }
// Output tied to one of several inputs
fn pick<'a>(first: &'a str, _hint: &str) -> &'a str { first }
The key skill is reading: each 'name is a label that ties some inputs and outputs together. Output lifetimes always come from inputs — Rust will not invent a reference out of thin air.
The Three Elision Rules
You almost never write lifetimes on common functions because the compiler applies elision rules.
- Each input reference gets its own lifetime parameter.
fn f(x: &T, y: &U)becomesfn f<'a, 'b>(x: &'a T, y: &'b U). - If there is exactly one input lifetime, it is assigned to every output reference.
- If one of the inputs is
&selfor&mut self, the lifetime ofselfis assigned to every output reference.
These cover the vast majority of methods and free functions.
// Rule 1 + 2 — compiler treats this as if you wrote <'a>(s: &'a str) -> &'a str
fn trim_start(s: &str) -> &str { s.trim_start() }
// Rule 3 — output tied to &self
impl Greeter {
fn name(&self) -> &str { &self.name }
}
When elision cannot produce a single unambiguous answer (such as longest above), you write the annotation yourself.
Lifetimes in Structs
If a struct stores a reference, you must declare a lifetime parameter on the struct. The annotation says “an instance of this struct cannot outlive the data it borrows.”
struct Excerpt<'a> {
text: &'a str,
}
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let e = Excerpt { text: first_sentence };
The Excerpt value borrows from novel, so e cannot live longer than novel. The compiler enforces this with the lifetime parameter.
Methods on such structs also need the parameter, though elision usually covers them.
impl<'a> Excerpt<'a> {
fn first_word(&self) -> &str {
self.text.split_whitespace().next().unwrap_or("")
}
}
The ‘static Lifetime
'static means “valid for the entire program.” String literals are &'static str because they are baked into the binary. You will sometimes see 'static as a bound on a generic, especially around threads and async tasks.
let s: &'static str = "hello"; // literal
Avoid sprinkling 'static everywhere just to silence the compiler. It usually points at a deeper ownership question — perhaps a value should be owned rather than borrowed.
A Worked Example
A function that returns the shorter of two slices.
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() <= b.len() { a } else { b }
}
fn main() {
let s1 = String::from("hello there");
let result;
{
let s2 = String::from("hi");
result = shortest(&s1, &s2);
println!("{}", result);
}
// result is no longer usable — s2 is gone
}
The shared lifetime 'a collapses to the shorter of s1 and s2’s scopes, so result is only valid inside the inner block. Move the println! outside and the compiler refuses, exactly as you would want.
When to Use Owned Data Instead
If you find yourself fighting lifetime annotations, consider returning an owned value (String, Vec<T>) rather than a borrowed slice. Owned data carries no borrow at all and bypasses these questions entirely. The cost is a small allocation; the benefit is simpler code and clearer ownership. For larger structures, the trade-off often goes the other way and references win — but for small results, owning is frequently the pragmatic answer.
A Note on Lifetime Subtyping
Sometimes you want to express “this lifetime outlives that one.” Rust spells this with 'a: 'b, read as “'a outlives 'b.” This is rarely needed in everyday code but worth recognizing in library signatures.
fn pick<'a, 'b: 'a>(short: &'a str, long: &'b str) -> &'a str {
if short.len() < long.len() { short } else { &long[..short.len()] }
}
Wrap up
Lifetimes describe how long references are valid; the compiler infers them whenever it can; you annotate when ambiguity blocks inference. Master the small set of patterns — single input, shared input, struct with reference — and lifetimes stop being scary. They become a language for stating, precisely, how the pieces of your program connect.