Skip to main content

Lifetimes

A lifetime describes the scope during which a reference is valid.

  • Every reference in Rust has a lifetime
  • Most of the time, the compiler infers it
  • Sometimes, you must explicitly tell the compiler how lifetimes relate

Lifetimes do not change how long values live They only describe relationships between references

Why Lifetimes Exist​

Without lifetimes, this would be allowed:

let r: &i32;

{
let x = 5;
r = &x;
}

println!("{}", r); // ❌ use-after-free

Rust prevents this by tracking lifetimes at compile time.

Lifetime Basics (Implicit Lifetimes)​

fn print(s: &String) {
println!("{}", s);
}

You didn’t write lifetimes, but Rust sees:

fn print<'a>(s: &'a String)
  • 'a = lifetime of the reference
  • The reference must be valid for the duration of the function call

When Lifetimes Become Necessary​

The Problem Case

fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}

❌ Compiler error: missing lifetime specifier

Why?

  • Rust doesn’t know whether the return value refers to x or y
  • It needs to know how long the returned reference is valid

Explicit Lifetime Annotations (Basic)​

Correct Version

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

What this means

  • 'a is a generic lifetime parameter
  • The returned reference is valid as long as both x and y are valid
  • The actual lifetime will be the shorter of the two at runtime

Usage

let s1 = String::from("short");
let s2 = String::from("much longer");

let result = longest(&s1, &s2);
println!("{}", result);

Safe, guaranteed.

Important Rule (This Is Key)​

A function cannot return a reference to a value it owns

fn bad() -> &String {
let s = String::from("hello");
&s // ❌ illegal
}

Why?

  • s is dropped at the end of the function
  • The reference would dangle

Lifetimes don’t “extend” data—they only describe reality.

Lifetime Elision Rules (Compiler Magic)​

Rust has rules that let you skip lifetime annotations in common cases.

Rule 1: One input reference → output uses same lifetime​

fn first(s: &str) -> &str {
s
}

Expanded form:

fn first<'a>(s: &'a str) -> &'a str

Rule 2: &self in methods gets its own lifetime​

impl String {
fn len_ref(&self) -> &usize {
// hypothetical example
}
}

Rust knows the output is tied to self.

Lifetimes with Structs (Intermediate)​

Struct Holding References​

struct ImportantExcerpt<'a> {
part: &'a str,
}

ImportantExcerpt cannot outlive the string it references

Usage

let novel = String::from("Call me Ishmael...");
let excerpt = ImportantExcerpt {
part: &novel[0..4],
};

println!("{}", excerpt.part);

If novel is dropped first → compile error.

Lifetimes in impl Blocks​

impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}

fn part(&self) -> &str {
self.part
}
}
  • &self uses the same lifetime 'a
  • Returned reference is guaranteed valid

Multiple Lifetimes (Intermediate)​

fn mix<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x
}

This is valid because:

  • Return value only depends on x
  • y’s lifetime is irrelevant to the output

But this would NOT compile:

fn bad<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
y // ❌ lifetime mismatch
}

Lifetimes + Mutable References​

fn replace<'a>(s: &'a mut String) -> &'a mut String {
s.push_str("!");
s
}
  • Only one mutable reference exists
  • Lifetime ensures no other borrows overlap

Lifetime Bounds (Advanced)​

'a: 'b (Outlives Relationship)

fn example<'a, 'b>(x: &'a str, y: &'b str)
where
'a: 'b,
{
// 'a lives at least as long as 'b
}

Used when one reference must outlive another.

Lifetimes with Traits (Advanced)​

use std::fmt::Display;

fn announce<'a, T>(x: &'a str, ann: T) -> &'a str
where
T: Display,
{
println!("Announcement: {}", ann);
x
}

Combines:

  • Lifetime parameters
  • Generic type parameters
  • Trait bounds

'static Lifetime (Special Case)​

'static = valid for the entire program

let s: &'static str = "I live forever";

Why?

  • String literals are embedded in the binary

Common Uses

fn takes_static(s: &'static str) {
println!("{}", s);
}

'static does NOT mean “no lifetime issues”
It means “this reference never becomes invalid”

Advanced: Lifetime vs Ownership (Key Insight)​

This does NOT work:

fn make_ref<'a>() -> &'a String {
let s = String::from("hello");
&s
}

Why?

  • Lifetimes don’t create ownership
  • You can’t return references to dropped values

Fix it by returning ownership:

fn make_string() -> String {
String::from("hello")
}

Mental Model That Actually Works​

  • Ownership: who owns the data?
  • Borrowing: who can access it?
  • Lifetimes: how long is that access valid?

Think of lifetimes as labels, not timers.

Why Lifetimes Matter​

Lifetimes guarantee:

  • No dangling references
  • No use-after-free
  • Thread-safe borrowing
  • Zero runtime cost

And all of this is enforced at compile time.