Skip to content

Lifetimes

Rust’s borrow checker prevents dangling references — references that point to memory that has already been freed. In simple cases the compiler can figure out how long a reference is valid on its own. But when a function takes two references and returns one of them, the compiler needs help: which input does the output borrow from, and for how long?

Lifetime annotations answer that question. They are written as 'a (a tick followed by a short name) inside angle brackets.

Here is a function that takes two string slices and returns the longer one. The return value borrows from one of the inputs — but which one? It depends on the runtime values. The annotation 'a says: “the return value lives at most as long as the shorter of the two inputs.”

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

All three references share the same lifetime 'a. The compiler uses this to ensure the returned reference never outlives the data it points to.

Without the annotation, the compiler would reject this function: it cannot determine the return lifetime from the inputs alone.

Lifetime annotations exist precisely to catch this class of bug at compile time. Here is a function that tries to return a reference to a value it created locally:

// @expect-error E0106
fn dangle() -> &String {
let s = String::from("hello"); // s is created here
&s // we return a reference to s
} // s is dropped here -- the reference would dangle
fn main() {}

The first error you get is not about dangling at all — it’s that the signature has no lifetime for the compiler to work with:

error[E0106]: missing lifetime specifier
--> src/main.rs:2:16
|
2 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
2 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
2 - fn dangle() -> &String {
2 + fn dangle() -> String {
|

If you take rustc’s first suggestion literally and add an explicit <'a> without changing anything else, the error changes — now the compiler is able to talk about the actual dangling-reference problem:

// @expect-error E0515
fn dangle<'a>() -> &'a String {
let s = String::from("hello"); // s is created here
&s // we return a reference to s
} // s is dropped here -- the reference would dangle
fn main() {}
error[E0515]: cannot return reference to local variable `s`
--> src/main.rs:4:5
|
4 | &s // we return a reference to s
| ^^ returns a reference to data owned by the current function

No lifetime annotation can fix this — 'a can only ever name a lifetime that already exists somewhere in the caller. dangle has no input reference to borrow 'a from, so there is nothing valid to name. The real fix, as rustc’s own second suggestion said, is to return an owned String instead of a reference.

What the Compiler Says: A Borrow That Outlives Its Data

Section titled “What the Compiler Says: A Borrow That Outlives Its Data”

E0106/E0515 catch a dangling return value. The same underlying problem shows up just as often with a local variable whose borrow is used after the variable itself is gone — caught as a different error, E0597:

// @expect-error E0597
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("{}", r);
}
error[E0597]: `x` does not live long enough
--> src/main.rs:5:13
|
4 | let x = 5;
| - binding `x` declared here
5 | r = &x;
| ^^ borrowed value does not live long enough
6 | }
| - `x` dropped here while still borrowed
7 | println!("{}", r);
| - borrow later used here

x is dropped at the end of the inner block, but r is used after that point — the compiler catches the use-after-free before your program can ever run it. E0106, E0515, and E0597 are three different diagnostics for the same root idea: a reference must never outlive the value it points to, whether the value would go away by returning from a function or by leaving a block.

If a struct holds a reference, you must annotate the lifetime so the compiler knows the struct cannot outlive the data it borrows:

struct Excerpt<'a> {
text: &'a str,
}

This says: “an Excerpt cannot outlive the string it borrows from.” The compiler enforces this guarantee automatically. Methods on a struct with a lifetime parameter repeat it in the impl line:

struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn announce(&self) -> &str {
self.text
}
}

impl<'a> Excerpt<'a> declares 'a for the block, then uses it to say “this impl is for an Excerpt borrowing for some lifetime 'a” — announce’s return type doesn’t need to repeat 'a explicitly here because of elision rule 3, covered next.

In many common patterns Rust can infer lifetimes automatically using three elision rules, so you do not need to write annotations every time. Each rule has a shape simple enough to show in isolation:

Rule 1 — every reference parameter gets its own, independent lifetime. This is the starting point the other two rules build on. When nothing needs to relate the parameters to each other or to the return type, rule 1 alone is enough:

fn describe_pair(a: &str, b: &str) -> String {
format!("{a} and {b}")
}

The compiler treats this as fn describe_pair<'a, 'b>(a: &'a str, b: &'b str) -> String — a and b get independent lifetimes 'a and 'b because the return type is an owned String that borrows nothing, so there is no output lifetime that needs to match either one.

Rule 2 — with exactly one input reference, its lifetime is assigned to every output reference.

fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}

Desugars to fn first_word<'a>(s: &'a str) -> &'a str — there is only one reference parameter, so any borrowed return value can only have come from it.

Rule 3 — if one parameter is &self or &mut self, its lifetime is assigned to every output reference, regardless of how many other parameters exist:

struct Wrapper { text: String }
impl Wrapper {
fn get(&self) -> &str {
&self.text
}
}

Desugars to fn get<'a>(&'a self) -> &'a str — the method borrows from self, so the returned &str is understood to borrow from self too.

longest from earlier needs an explicit annotation precisely because none of these three rules apply to it: it has two reference parameters (so rule 2 doesn’t apply — there isn’t “exactly one”), and neither is &self (so rule 3 doesn’t apply). Only when all three rules taken together fully determine the output lifetime does Rust let you omit the annotation.

flowchart TD
  Start["Function has reference parameters and/or a returned reference"]
  R1["Rule 1: give each input reference its own lifetime"]
  R2{"Exactly one input reference?"}
  R3{"Is one parameter '&self' or '&mut self'?"}
  Ambiguous["Output lifetime is ambiguous -- write it explicitly"]
  Resolved["Elision succeeds -- no annotation needed"]
  Start --> R1 --> R2
  R2 -->|Yes| Resolved
  R2 -->|No| R3
  R3 -->|Yes| Resolved
  R3 -->|No| Ambiguous
The three elision rules as one decision

'static is a special, named lifetime meaning “valid for the entire duration of the program.” You’ve already seen it without the name: every string literal is &'static str, because literals are baked directly into the compiled binary and are never freed.

let s: &'static str = "I have a static lifetime.";

'static shows up in two very different situations, and it’s worth telling them apart:

  • References to genuinely static data — string literals, and values in a static item — are 'static for free, at no cost, because that data really does live for the whole program.
  • T: 'static as a trait bound (common on generic parameters, especially with Box<dyn Trait + 'static> or when spawning threads) means “this type contains no non-'static references” — most owned types (String, Vec<T>, i32) satisfy this trivially, since they own their data rather than borrowing it. It does not mean “lives forever” for an owned value; it means the value isn’t secretly borrowing from something that could go away.

Reaching for 'static to silence a lifetime error is a common but usually wrong instinct — it rarely fixes a genuine dangling-reference problem (as the dangle example above shows, rustc’s own 'static suggestion does not actually solve anything), and more often means the code should own its data instead of borrowing it.

Returning impl Trait: A Lifetime Feature in Disguise

Section titled “Returning impl Trait: A Lifetime Feature in Disguise”

The Traits lesson’s impl Trait return-position example is really a lifetime feature: in the Rust 2024 edition, a returned impl Trait automatically captures every lifetime in scope, so a function borrowing from a parameter needs no + '_ annotation the way it did on edition = "2021" (error[E0700]). The opt-out for “capture nothing” is the empty use<> bound shown there. If you haven’t seen that example yet, it’s worth reading now that you understand elision — it is the same “which lifetime does the output relate to” question, just answered automatically by the edition instead of by an explicit 'a.

Every lifetime you’ve annotated so far is chosen once, by the caller, at the point a function is called. Sometimes you need the opposite: a bound that must hold for every possible lifetime, not just one specific one — most commonly when accepting a closure that will be called with a freshly borrowed reference each time. This is a higher-ranked trait bound, written for<'a>:

fn apply_to_local<F>(f: F) -> i32
where
F: for<'a> Fn(&'a i32) -> i32,
{
let x = 10;
f(&x)
}
fn main() {
let double = |n: &i32| n * 2;
println!("{}", apply_to_local(double));
}
20

for<'a> Fn(&'a i32) -> i32 means “for any lifetime 'a the caller of f picks, f must accept a &'a i32” — apply_to_local creates x and borrows it inside the function, so no single lifetime named up front could describe it. In practice you rarely write for<'a> by hand; the compiler infers it automatically for closure parameters like this one, and you mostly meet the syntax when reading a compiler error or library documentation rather than writing it yourself.

Variance: A &'static str Fits Anywhere a Shorter &'a str Is Expected

Section titled “Variance: A &'static str Fits Anywhere a Shorter &'a str Is Expected”

A reference with a longer lifetime can always be used where a shorter one is expected — this property is called variance, and it’s why the following compiles with no coercion syntax at all:

fn main() {
let s: &'static str = "hello";
let owned = String::from("world");
let r: &str = &owned;
fn takes_short<'a>(x: &'a str) -> &'a str { x }
println!("{}", takes_short(s)); // a &'static str satisfies any shorter &'a
println!("{}", takes_short(r));
}
hello
world

Think of a lifetime as a guarantee of “valid for at least this long” — 'static is the strongest possible guarantee, so it satisfies any weaker requirement automatically. This relationship only goes one direction: you cannot pass a short-lived &'a str where a &'static str is specifically required, since a shorter guarantee cannot stand in for a stronger one.

// A function with a lifetime annotation:
// the returned reference lives at least as long as the shorter of x or y.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// A struct that holds a reference — must annotate the lifetime.
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn announce(&self) -> &str {
self.text
}
}
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("longest: {}", result);
}
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find '.'");
let excerpt = Excerpt { text: first_sentence };
println!("excerpt: {}", excerpt.announce());
}

A lifetime is not a duration in seconds or a runtime concept at all — it is a name the compiler gives to a region of code where a particular borrow is valid, so it can compare regions against each other (“does this borrow’s region end before that value is dropped?”). Elision rules are just the compiler’s built-in guesses for the most common shapes of that comparison; when a function’s shape doesn’t match one of those guesses, you supply the name yourself so the comparison has something to check.

Explicit lifetime annotations are the most syntactically unusual part of Rust for newcomers, and elision exists specifically to minimize how often you need to write them — most real functions never need an explicit 'a at all. What you get in exchange is dangling-reference bugs (use-after-free, returning a pointer to a freed stack frame) moving from “a runtime crash or, worse, silent memory corruption in C/C++” to “a compile error with a specific line number,” at zero runtime cost — lifetimes are completely erased before the program runs.

  • “Lifetime annotations make a value live longer.” They do the opposite of “control” lifetime — they only describe an existing relationship between borrows for the compiler to check; they cannot extend how long any value actually stays alive.
  • “Adding 'static to a struct field or bound is a safe way to make a lifetime error go away.” As the dangle and Cache examples show, 'static either fails to compile for an unrelated reason or forces every caller to only pass in data that genuinely lives for the whole program — it is almost never the fix a borrow-related error is actually asking for.
  • “for<'a> is something I’ll need to write constantly once I use closures.” The compiler infers higher-ranked trait bounds automatically for the overwhelming majority of closure parameters; you mainly encounter the syntax itself while reading, not writing, function signatures.
  • “A &'static str needs an explicit conversion to be used where a &'a str is expected.” No conversion or coercion syntax is needed — this is what variance guarantees: a longer-lived reference already satisfies any shorter requirement.

When a function triggers a lifetime error, generated code often “fixes” it by adding a 'static bound to a generic parameter or the struct’s own lifetime, instead of restructuring so the value is owned or borrowed for a shorter, correct duration.

🐛 Spot the bug

struct Cache<'a> {
data: &'a str,
}
fn build_cache(input: &str) -> Cache<'static> {
Cache { data: input }
}
fn main() {
let text = String::from("local data");
let cache = build_cache(&text);
println!("{}", cache.data);
}
Show the answer

This does not compile — error: lifetime may not live long enough, pointing at Cache { data: input } and explaining that input’s lifetime “must outlive 'static.” This is exactly the shape of fix an AI assistant reaches for when a lifetime error appears: slap 'static on the return type so the error looks “handled.” But input: &str here can be a reference to any short-lived local data — the function has no way to promise the caller’s data lives forever, and the compiler catches the lie at the point the (impossible) promise is made, before the caller-side bug it would otherwise cause. There is no annotation that fixes this version: either the function should return Cache<'a> tied to the actual input lifetime (fn build_cache<'a>(input: &'a str) -> Cache<'a>), or Cache should own a String instead of borrowing a &str at all.

Why do lifetime annotations exist in Rust?
In the function signature fn longest<'a>(x: &'a str, y: &'a str) -> &'a str, what does the 'a annotation guarantee?
Which of the three elision rules applies to fn get(&self) -> &str on some struct, letting it compile with no explicit lifetime?
Does this compile?
fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("{}", r);
}
What does this program print?
fn main() {
    let s: &'static str = "hello";
    fn takes_short<'a>(x: &'a str) -> &'a str { x }
    println!("{}", takes_short(s));
}
Why does adding 'static to Cache<'static> in fn build_cache(input: &str) -> Cache<'static> { Cache { data: input } } fail to compile?
What is a higher-ranked trait bound like for<'a> Fn(&'a i32) -> i32 used for?