Lifetimes in Rust
Lifetimes are one of Rust's most distinctive features — and one that trips up newcomers more than almost anything else. The good news: lifetimes are not magic. They are annotations that help the borrow checker verify that every reference in your program is valid at the time it is used.
A lifetime does not change how long data lives. It is simply a label that describes an existing relationship: "this reference must not outlive that data."
The Problem: Dangling References
Before looking at the syntax, it helps to see the problem lifetimes solve. A dangling reference is a reference that points to memory that has already been freed. In languages without a borrow checker this is a silent, catastrophic bug. Rust's compiler refuses to compile code that would create one.
fn main() {
let r; // declare r — no value yet
{
let x = 5;
r = &x; // r borrows x
} // x is dropped here — r now dangles
println!("{}", r); // ERROR: x does not live long enough
}error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5 | let x = 5;
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
9 | println!("{}", r);
| - borrow later used herex) with the lifetime of the reference (r). Because x is dropped before r is used, the compiler rejects the program.Visualising Lifetimes as Scopes
You can think of a lifetime as the span of code during which a reference is valid. The borrow checker ensures that no reference outlives the data it points to.
// BAD — r outlives x
fn main() {
let r; // ------+-- r's lifetime begins
{ // |
let x = 5; // -+ | x's lifetime begins
r = &x; // | |
} // -+ | x dropped — r still alive!
println!("{}", r); // ------+ COMPILER ERROR
}
// GOOD — r lives within x
fn main() {
let x = 5; // ------+-- x's lifetime begins
let r = &x; // ---+-- r's lifetime begins (nested inside x)
println!("{}", r); // | used here — x still alive: OK
} // ------+-- both droppedLifetime Annotations: The 'a Syntax
When the compiler cannot figure out the relationship between reference lifetimes on its own — most often in function signatures — you must annotate them explicitly.
Lifetime parameters begin with an apostrophe and are usually short lowercase names:
'a, 'b, 'input. They are placed after the & of a reference type:
&str // a reference — no lifetime annotation &'a str // a reference with explicit lifetime 'a &'a mut str // a mutable reference with explicit lifetime 'a
'a does not create or lengthen a lifetime — it tells the compiler "whatever lifetime this reference has, call it 'a."Lifetime Annotations in Function Signatures
The classic example is a function that returns the longer of two string slices.
Without lifetime annotations the compiler cannot tell whether the returned reference
comes from x or y, and therefore cannot verify that the caller uses it safely.
// This does NOT compile — the compiler cannot determine the return lifetime
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
// Correct version — annotated with 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}The annotation <'a> says: "for some lifetime 'a, both x and y live at
least as long as 'a, and the returned reference also lives at least as long as 'a."
In practice 'a is the shorter of the two input lifetimes — the borrow checker
picks the most conservative valid region.
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("longest: {}", result); // OK — result used inside s2's scope
}
// println!("{}", result); // would ERROR — s2 dropped, result could dangle
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}longest: long string
Lifetimes in Structs
Structs can hold references, but every reference field must have a lifetime annotation. This prevents the struct from outliving the data it references.
struct Important<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("no period found");
let excerpt = Important { part: first_sentence };
println!("Important part: {}", excerpt.part);
} // novel and excerpt are both dropped here — safeImportant part: Call me Ishmael
The annotation <'a> on Important says: "an Important instance cannot outlive
the reference stored in its part field." If you tried to move excerpt out of the
scope where novel lives, the compiler would reject it.
Lifetimes in impl Blocks
When implementing methods on a struct that has lifetime parameters, you must declare
the lifetime in the impl block and repeat it after the struct name.
struct Important<'a> {
part: &'a str,
}
impl<'a> Important<'a> {
// Method that returns a reference — lifetime elision applies (see next page)
fn announce(&self, announcement: &str) -> &str {
println!("Attention: {}!", announcement);
self.part
}
}
fn main() {
let text = String::from("some important text");
let imp = Important { part: &text };
let part = imp.announce("listen up");
println!("The important part: {}", part);
}Attention: listen up! The important part: some important text
The 'static Lifetime
The special lifetime 'static means the reference is valid for the entire duration
of the program. String literals are the most common example — they are stored directly
in the program's binary and therefore always available.
// String literals have type &'static str
let s: &'static str = "I live forever";
// Functions can require 'static when storing references long-term
fn needs_static(s: &'static str) {
println!("{}", s);
}
fn main() {
needs_static("hello"); // OK — string literal is 'static
let owned = String::from("world");
// needs_static(&owned); // ERROR — &owned is not 'static
}'static to fix a lifetime error, think carefully before doing so. It usually means your design needs to own the data (e.g. use String instead of &str) rather than keep a reference around indefinitely.Multiple Lifetime Parameters
A function can have multiple independent lifetime parameters. This is useful when the return type is tied to only one of the inputs.
// The return value's lifetime is tied to x, not y
fn mixed<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
println!("y is: {}", y);
x
}
fn main() {
let s1 = String::from("hello");
let result;
{
let s2 = String::from("world");
result = mixed(s1.as_str(), s2.as_str());
// s2 can be dropped because result's lifetime only depends on s1
}
println!("result: {}", result); // OK — s1 still alive
}y is: world result: hello
Lifetime Bounds on Generics
When combining generics with references you sometimes need to constrain the generic
type to outlive a given lifetime. The syntax is T: 'a, which means "T must live
at least as long as 'a."
use std::fmt::Display;
// T must implement Display and live at least as long as 'a
fn print_ref<'a, T>(val: &'a T)
where
T: Display + 'a,
{
println!("{}", val);
}
struct Wrapper<'a, T: 'a> {
value: &'a T,
}
fn main() {
let number = 42;
print_ref(&number);
let w = Wrapper { value: &number };
println!("wrapped: {}", w.value);
}42 wrapped: 42
Common Misconceptions
Lifetimes do not change how long data lives. The compiler never extends or shortens the actual memory lifetime of a value — it only checks that references are used correctly within existing lifetimes.
Lifetime annotations are not runtime overhead. They exist entirely in the type system and are erased before compilation to machine code.
You do not always need to write lifetimes. The compiler infers them in many common patterns via lifetime elision (covered on the next page).
A longer lifetime is not always better. Tying a reference to a longer lifetime can prevent data from being freed, causing unnecessary memory use.
'staticis not a magic fix. If the compiler complains about lifetimes, making everything'staticsidesteps the issue but usually indicates a design problem.
Combining Lifetimes, Generics, and Trait Bounds
All three features compose naturally. Here is a function that uses generics, a trait bound, and a lifetime together — a pattern common in real-world Rust libraries.
use std::fmt::Display;
fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
announcement: T,
) -> &'a str
where
T: Display,
{
println!("Announcement: {}", announcement);
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("long string");
let s2 = String::from("xyz");
let result = longest_with_announcement(
s1.as_str(),
s2.as_str(),
"Today's winner is...",
);
println!("Longest: {}", result);
}Announcement: Today's winner is... Longest: long string
How the Borrow Checker Uses Lifetimes
The borrow checker's job is to ensure that for every reference in your program, the data it points to is still alive. It does this by:
- Assigning a lifetime to every reference (either inferred or from your annotation).
- Checking that the region where the reference is used is contained within the lifetime of the data.
- Rejecting the program if any reference could outlive its data.
Lifetime annotations in function signatures give the borrow checker the information it needs to verify call sites without looking at the function body — only at the signature.
Borrow checker flow for longest<'a>(x: &'a str, y: &'a str) -> &'a str
Call site: longest(s1.as_str(), s2.as_str())
| |
Lifetimes: 'a = lifetime of s1 'a = lifetime of s2
-> 'a is the SHORTER of the two
-> return value must be used within that shorter region
Checker verifies: is the return value used only while both s1 and s2 are alive?
If yes: compile. If no: error.Quick Reference
Pattern | Syntax | Meaning |
|---|---|---|
Lifetime parameter | fn f<'a>(x: &'a str) | 'a names the lifetime of x |
Multiple lifetimes | fn f<'a, 'b>(x: &'a str, y: &'b str) | x and y may have different lifetimes |
Struct with reference | struct S<'a> { f: &'a str } | S cannot outlive its field |
impl block | impl<'a> S<'a> { ... } | Lifetime must be declared on impl |
Static lifetime | &'static str | Reference valid for entire program |
Lifetime bound | T: 'a | T must live at least as long as a |
Multiple bounds | T: Display + 'a | T implements Display and lives >= a |