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.