Skip to content

Lifetimes

Rust’s borrow checker must ensure that every reference is valid for its entire use. Without lifetime Annotations, the compiler cannot prove that a reference outlives the scope in which it is used. This Prevents dangling references — references to memory that has been freed or invalidated.

Consider the canonical dangling reference attempt:

fn dangle() -> &String {
let s = String::from("hello");
&s
}

The compiler rejects this because s is dropped at the end of dangleBut the function promises To return a reference. The returned reference would point to freed memory. Lifetimes are the Mechanism by which the compiler tracks and enforces this constraint.

Every reference in Rust has a lifetime — a region of code during which the reference is valid. In Most cases, the compiler infers lifetimes automatically. Explicit annotations are needed when the Relationship between input and output lifetimes is ambiguous.

Lifetimes use a leading apostrophe followed by a name. By convention, 'a is the first lifetime, 'b the second, and so on. The name is purely a compile-time label — it has no runtime Representation.

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

This signature says: “there exists some lifetime 'a such that both x and y live at least as Long as 'aAnd the returned reference also lives at least as long as 'a.” The caller chooses The concrete lifetime, constrained by the actual lifetimes of the arguments.

let result;
let s1 = String::from("long string");
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("longest: {}", result);
}
// result is valid here because its lifetime is bounded by s1's lifetime

Functions can have multiple independent lifetime parameters:

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

The return type’s lifetime is tied only to 'a. The compiler does not require 'a and 'b to have Any relationship — they are independent.

The relationship between input and output lifetimes determines how references flow through a Function:

// Output lives as long as x
fn first_word<'a>(text: &'a str) -> &'a str {
let end = text.find(' ').unwrap_or(text.len());
&text[..end]
}
// Output lives as long as the shorter of x and y
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Output lives as long as x, ignoring y's lifetime
fn first<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
x
}

'static means the reference lives for the entire duration of the program. All string literals have 'static lifetime because they are embedded in the binary:

let s: &'static str = "hello";
let s: &str = "hello"; // &'static is inferred for literals
## Intuition

Lifetimes are Rust’s way of tracking how long references remain valid. Every reference has a lifetime that the compiler infers or you annotate explicitly. Lifetime elision rules reduce boilerplate in common patterns. The ‘static lifetime means a reference lives for the entire program duration. Lifetimes prevent dangling references by ensuring data outlives the pointers that reference it, and they enable safe borrowing across function boundaries without runtime checks.

  • [[rust/02-ownership-borrowing/ownership]] - Move semantics and the borrow checker
  • [[rust/02-ownership-borrowing/interior-mutability]] - Mutability within shared references
  • [[rust/04-error-handling/error-handling]] - Result with lifetime annotations
  • [[rust/05-traits-generics/traits-and-generics]] - Generic bounds with lifetime constraints