Lifetimes
Lifetimes are how Rust knows that every reference points to valid data. They are not a new concept — every reference in every language has a lifetime. Rust just makes you think about them explicitly when the compiler cannot figure them out on its own. Most of the time, lifetimes are inferred. When they are not, the annotation syntax is straightforward once you understand what it means.
What Lifetimes Are
A lifetime is the scope during which a reference is valid. The compiler tracks these automatically in most cases:
fn main() {
let r; // r declared here
{
let x = 5;
r = &x; // r borrows x
} // x is dropped here
// println!("{}", r); // error: x does not live long enough
}
The reference r would outlive the data it points to. The compiler rejects this. No dangling pointer, no segfault, no undefined behavior.
When the compiler can see both the reference and the data it points to, it handles lifetimes automatically. Problems arise when references cross function boundaries — the compiler needs help knowing how long the returned reference is valid.
The 'a Notation
Lifetime annotations use a tick followed by a name: 'a, 'b, 'input, etc. They do not change how long data lives. They describe relationships between reference lifetimes so the compiler can verify safety.
// This says: the returned reference lives as long as both input references
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("hi");
result = longer(&s1, &s2);
println!("Longer: {}", result); // works: both s1 and s2 are alive
}
// println!("{}", result); // would fail: s2 is dropped, result might refer to it
}
Longer: long string
The 'a in longer means: the returned reference is valid for the shorter of the two input lifetimes. The compiler uses this to verify that you never use the result after either input is dropped.
Lifetime Elision
You do not need to annotate lifetimes most of the time. The compiler applies three elision rules:
- Each reference parameter gets its own lifetime.
- If there is exactly one input lifetime, it is assigned to all output references.
- If one of the parameters is
&selfor&mut self, its lifetime is assigned to all output references.
// You write this:
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// The compiler sees this (rule 1 + rule 2):
// fn first_word<'a>(s: &'a str) -> &'a str
// You write this:
fn first_char(s: &str) -> Option<char> {
s.chars().next()
}
// No lifetime annotation needed — return type contains no references
fn main() {
let text = String::from("hello world");
let word = first_word(&text);
println!("{}", word);
}
hello
Elision covers the vast majority of cases. You only need explicit annotations when the compiler cannot determine the relationship between input and output lifetimes.
When Lifetimes Get Explicit
Lifetimes become necessary when a function takes multiple references and returns one:
// Won't compile without lifetimes — compiler doesn't know which
// input the output is tied to:
// fn pick(a: &str, b: &str, use_first: bool) -> &str
// With lifetimes:
fn pick<'a>(a: &'a str, b: &'a str, use_first: bool) -> &'a str {
if use_first { a } else { b }
}
fn main() {
let a = String::from("alpha");
let b = String::from("beta");
let chosen = pick(&a, &b, true);
println!("{}", chosen);
}
alpha
If the output can only come from one input, you can be more precise:
// The return value only ever comes from `text`, not `prefix`
fn strip_prefix<'a>(text: &'a str, prefix: &str) -> &'a str {
text.strip_prefix(prefix).unwrap_or(text)
}
fn main() {
let text = String::from("hello_world");
let result;
{
let prefix = String::from("hello_");
result = strip_prefix(&text, &prefix);
} // prefix dropped, but that's fine — result refers to text
println!("{}", result);
}
world
Here, prefix has a different (shorter) lifetime than text, and that is fine because the return value is tied only to text.
Structs Holding References
Structs can hold references, but they must have lifetime annotations:
#[derive(Debug)]
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn new(text: &'a str) -> Self {
Excerpt { text }
}
fn words(&self) -> Vec<&str> {
self.text.split_whitespace().collect()
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = Excerpt::new(first_sentence);
println!("{:?}", excerpt);
println!("Words: {:?}", excerpt.words());
}
Excerpt { text: "Call me Ishmael" }
Words: ["Call", "me", "Ishmael"]
The lifetime 'a on Excerpt means: this struct cannot outlive the string it references. If novel is dropped while excerpt still exists, the compiler catches it.
The 'static Lifetime
'static means the reference lives for the entire program. String literals have this lifetime:
fn get_greeting() -> &'static str {
"Hello, world!" // string literal, baked into the binary
}
// Leaked heap data also gets 'static lifetime (use sparingly):
fn leaked_string() -> &'static str {
let s = String::from("I live forever");
Box::leak(s.into_boxed_str())
}
fn main() {
println!("{}", get_greeting());
println!("{}", leaked_string());
}
Hello, world!
I live forever
'static does not mean immortal or immutable — it means the reference is valid for as long as it could possibly be needed. Trait objects sent across threads often require 'static because the compiler cannot know when the thread will finish.
The Pragmatic Solution: Own the Data
When lifetimes get complex, the simplest fix is often to stop borrowing and start owning:
// Complex: struct borrows data, lifetime annotations everywhere
struct BorrowedConfig<'a> {
host: &'a str,
port: u16,
}
// Simple: struct owns its data, no lifetimes needed
struct OwnedConfig {
host: String,
port: u16,
}
// In practice, OwnedConfig is almost always what you want.
// The cost of owning a String vs borrowing a &str is negligible
// for configuration data.
fn load_config() -> OwnedConfig {
OwnedConfig {
host: String::from("localhost"),
port: 8080,
}
}
fn main() {
let config = load_config();
println!("{}:{}", config.host, config.port);
}
localhost:8080
Borrowing in structs is useful for performance-critical code that processes large data without copying. For most application code, owning the data is simpler and the performance difference is irrelevant.
Lifetime Bounds on Generics
Generic types can have lifetime bounds:
use std::fmt::Display;
fn announce<'a, T: Display>(value: &'a T, label: &str) -> &'a T {
println!("{}: {}", label, value);
value
}
fn main() {
let number = 42;
let result = announce(&number, "The answer");
println!("Got back: {}", result);
}
The answer: 42
Got back: 42
Real-World Example: Parsing Without Allocation
Lifetimes enable zero-copy parsing — you parse a large input and return references into the original data without allocating new strings:
#[derive(Debug)]
struct HttpHeader<'a> {
name: &'a str,
value: &'a str,
}
fn parse_header(line: &str) -> Option<HttpHeader<'_>> {
let colon = line.find(':')?;
Some(HttpHeader {
name: line[..colon].trim(),
value: line[colon + 1..].trim(),
})
}
fn parse_headers(raw: &str) -> Vec<HttpHeader<'_>> {
raw.lines()
.filter_map(parse_header)
.collect()
}
fn main() {
let raw = "Content-Type: application/json\nContent-Length: 42\nHost: example.com";
let headers = parse_headers(raw);
for h in &headers {
println!("{}: {}", h.name, h.value);
}
}
Content-Type: application/json
Content-Length: 42
Host: example.com
No strings were allocated. Every name and value is a reference into the original raw string. For a high-performance HTTP parser, this matters.
Common Pitfalls
- Adding lifetimes when you should own the data. If a struct lives longer than the data it references, you need
Stringnot&str. Most application structs should own their data. - Thinking lifetimes change how long data lives. They do not. They only describe relationships. Adding
'adoes not extend or shorten anything. - Using
'staticas an escape hatch. Requiring'staticon everything defeats the purpose of Rust's lifetime system. It usually means you should rethink your data ownership. - Panicking when you see complex lifetime errors. Read the error message. The compiler tells you which reference outlives which data. Follow the trail.
- Over-annotating lifetimes. If elision handles it, do not add explicit annotations. They add noise without adding information. Let the compiler do its job.
Key Takeaways
- Lifetimes ensure every reference points to valid data. They are checked at compile time with zero runtime cost.
- Lifetime elision handles most cases automatically. You only annotate when the compiler asks.
- The
'asyntax describes relationships between references — it does not control how long data lives. - Structs holding references need lifetime annotations, but consider owning the data instead.
'staticmeans the reference is valid for the entire program. String literals are'static.- When lifetimes get complicated, the pragmatic solution is often to clone or own the data. Profile before optimizing with borrows.