Free Handbook · Every example compiled & verified

Lifetimes

Why Rust tracks how long references live, how to read and write 'a annotations, the elision rules that hide them, and the three lifetime errors you will meet.

0 / 140 lessons🔥 0 day streak
ShareXLinkedIn

Module 08 · what you'll be able to do

  • Explain what a dangling reference is and how the borrow checker stops one at compile time
  • Add lifetime annotations to a function that returns a reference and say what they promise
  • Apply the three elision rules to predict when you can leave lifetimes out
  • Write a struct that holds a reference and implement methods on it
  • Read and fix E0106, E0597 and E0515, and know when 'static is the right answer and when it is a trap
01

Why lifetimes exist

A reference (&T) is a pointer that does not own what it points at. Something else owns the value, and when that owner goes out of scope the value is dropped. If a reference is still around after that, it points at freed memory: a dangling reference. In C and C++ this compiles and then crashes, or worse, silently reads garbage. Rust refuses to compile it.

A lifetime is the stretch of code during which a reference is valid. The compiler part that checks lifetimes is the borrow checker (you met it in Module 03). Its one rule: a reference must never outlive the value it borrows from. Most of the time it works out every lifetime on its own and you never write one. You only annotate when the compiler cannot tell which input a returned reference came from.

rustmain.rs
fn main() {
    let owner = String::from("borrowed text");
    let r = &owner;          // r borrows from owner
    println!("{}", r);       // fine: owner is still alive
    println!("{} bytes", r.len());
}   // r stops being used, then owner is dropped here
Outputcompiled & run with real Rust
borrowed text
13 bytes

The reference is used only while the owner is alive, so there is nothing for the borrow checker to complain about.

Your turn

Move let owner into an inner block { ... } and keep the println! outside it. Read the error before looking at the next card.

Error you will hit

E0597: a reference that outlives its value

rust
fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("r = {}", r);
}
error[E0597]: `x` does not live long enough
 --> 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 = {}", r);
  |                        - borrow later used here
Why the compiler said that

x lives only inside the inner block and is dropped at the closing brace on line 6. r is used on line 7, after that. The error message literally draws the three facts: where the borrow starts, where the owner dies, and where the borrow is used too late.

The fix

Make the owner live at least as long as the reference: declare it in the outer scope (or copy the value out instead of borrowing it).

rust
fn main() {
    let x = 5;
    let r = &x;
    println!("r = {}", r);
}
VisualizeWhere each lifetime starts and endsStep 1 / 4
fn main() {
let x = 5;
let r = &x;
println!("r = {}", r);
}
Line 2

x is created. Its lifetime runs from here to the closing brace of main.

Variables now
x5
All 4 steps as a table
StepLineWhat happenedVariables now
12x is created. Its lifetime runs from here to the closing brace of main.x = 5
23r borrows x. The borrow checker records that this borrow must end before x does.x = 5 r = &x
34Last use of r. The borrow ends here, well inside the lifetime of x, so the program is accepted.x = 5 r = &x
45x is dropped. No reference to it is alive any more.
02

Lifetime annotations in functions

When a function takes two references and returns one, the compiler cannot see from the signature which input the result borrows from. Callers need to know that, because it decides how long they must keep each argument alive. So Rust asks you to say it.

Error you will hit

E0106: missing lifetime specifier

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

fn main() {
    println!("{}", longest("apple", "fig"));
}
error[E0106]: missing lifetime specifier
 --> main.rs:1:33
  |
1 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++
Why the compiler said that

The result is sometimes x and sometimes y. Without an annotation the compiler has no rule that tells it which one the caller must keep alive, so it stops and asks.

The fix

Do what the help line says: introduce a lifetime parameter and tie both inputs and the output to it.

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

<'a> declares a lifetime parameter, just like <T> declares a type parameter. &'a str reads "a string slice that is valid for at least 'a". The signature below promises: the returned reference is valid for as long as both inputs are valid. In practice 'a becomes the shorter of the two lifetimes. Annotations never change how long anything lives; they only describe relationships so the checker can verify them.

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

fn main() {
    let a = String::from("a long string");
    let b = String::from("xyz");
    let result = longest(a.as_str(), b.as_str());
    println!("longest: {}", result);

    let c = String::from("inner");
    {
        let d = String::from("tiny");
        println!("longest: {}", longest(c.as_str(), d.as_str()));
    }
}
Outputcompiled & run with real Rust
longest: a long string
longest: inner

The second call is fine because the result is used inside the block where both c and d are still alive.

Your turn

Write fn first<'a>(x: &'a str, _y: &str) -> &'a str that always returns x. Notice that _y needs no lifetime tie, because the result never borrows from it.

Error you will hit

E0597: the caller breaks the promise

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

fn main() {
    let a = String::from("a long string");
    let result;
    {
        let b = String::from("xyz");
        result = longest(a.as_str(), b.as_str());
    }
    println!("{}", result);
}
error[E0597]: `b` does not live long enough
  --> main.rs:10:38
   |
 9 |         let b = String::from("xyz");
   |             - binding `b` declared here
10 |         result = longest(a.as_str(), b.as_str());
   |                                      ^ borrowed value does not live long enough
11 |     }
   |     - `b` dropped here while still borrowed
12 |     println!("{}", result);
   |                    ------ borrow later used here
Why the compiler said that

We can see that a is longer and will be returned, but the signature says the result may borrow from either input. The compiler checks against the signature, not against the values at runtime, so result is only allowed to live as long as the shorter-lived b.

The fix

Use the result inside the inner block, or move b out so it lives as long as result.

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

fn main() {
    let a = String::from("a long string");
    let b = String::from("xyz");
    let result = longest(a.as_str(), b.as_str());
    println!("{}", result);
}
03

Returning references to local data (E0515)

A returned reference must borrow from one of the inputs (or from something 'static). It can never borrow from a local variable, because every local is dropped when the function returns. No annotation can fix that; the answer is to return an owned value instead.

Error you will hit

E0515: cannot return reference to local variable

rust
fn make_greeting(name: &str) -> &str {
    let s = format!("Hello, {}!", name);
    &s
}

fn main() {
    println!("{}", make_greeting("Ada"));
}
error[E0515]: cannot return reference to local variable `s`
 --> main.rs:3:5
  |
3 |     &s
  |     ^^ returns a reference to data owned by the current function
Why the compiler said that

s is a String created inside the function. It is dropped when the function returns, so &s would point at freed memory. Note the signature compiled fine: with one input reference, elision tied the output to name, and the body broke that promise.

The fix

Return the owned String. Ownership moves out to the caller; nothing is copied and nothing dangles.

rust
fn make_greeting(name: &str) -> String {
    format!("Hello, {}!", name)
}

fn main() {
    println!("{}", make_greeting("Ada"));
}
rustmain.rs
fn first_word(s: &str) -> &str {
    s.split(' ').next().unwrap_or("")
}

fn shout(s: &str) -> String {
    s.to_uppercase()
}

fn main() {
    let line = String::from("borrow checker rules");
    let w = first_word(&line);   // borrows from line
    let loud = shout(w);         // brand new owned String
    println!("{} -> {}", w, loud);
}
Outputcompiled & run with real Rust
borrow -> BORROW

first_word returns a slice of its input, so a reference is correct. shout builds new text, so it must return an owned String.

Your turn

Add fn last_word(s: &str) -> &str using s.split(' ').last() and print it for the same line.

The quick decision
Does the result point into an argument (a slice, a field, an element)? Return a reference. Does the function build something new? Return an owned type: String, Vec<T>, a struct.
04

Lifetime elision: why you rarely write 'a

Early Rust made you annotate every reference in every signature. The patterns were so repetitive that the compiler now fills them in using three elision rules. If the rules produce an answer, you write nothing. If they do not, you get E0106.

  1. 1
    Rule 1: every input reference gets its own lifetime

    fn f(x: &str, y: &str) is read as fn f<'a, 'b>(x: &'a str, y: &'b str).

  2. 2
    Rule 2: one input lifetime flows to every output

    If there is exactly one input lifetime, the output gets it. fn first_word(s: &str) -> &str becomes fn first_word<'a>(s: &'a str) -> &'a str.

  3. 3
    Rule 3: methods borrow from self

    If one of the inputs is &self or &mut self, the output gets the lifetime of self, however many other reference parameters there are.

Signature you writeWhat the compiler readsNeeds annotation?
fn len(s: &str) -> usizeNo output reference, nothing to decideNo
fn trim(s: &str) -> &strfn trim<'a>(s: &'a str) -> &'a str (rule 2)No
fn name(&self, p: &str) -> &strOutput tied to self (rule 3)No
fn pick(a: &str, b: &str) -> &strTwo input lifetimes, no self: no rule appliesYes (E0106)
rustmain.rs
// Written with elision...
fn trim_dots(s: &str) -> &str {
    s.trim_matches('.')
}

// ...and exactly what the compiler reads it as.
fn trim_dots_explicit<'a>(s: &'a str) -> &'a str {
    s.trim_matches('.')
}

fn main() {
    let raw = String::from("...ready...");
    println!("{}", trim_dots(&raw));
    println!("{}", trim_dots_explicit(&raw));
}
Outputcompiled & run with real Rust
ready
ready
Your turn

Add a second parameter c: char to trim_dots and trim that character instead. Does it still compile without annotations? (Yes: char is not a reference, so there is still one input lifetime.)

Quick check

Which signature compiles without writing any lifetime?

05

Structs that hold references

A struct field can be a reference, but then the struct itself must not outlive what that field points at. You express this with a lifetime parameter on the struct: struct Excerpt<'a> { part: &'a str } reads "an Excerpt cannot outlive the text its part borrows from".

Error you will hit

E0106 on a struct field

rust
struct Excerpt {
    part: &str,
}

fn main() {
    let text = String::from("Call me Ishmael. Some years ago...");
    let e = Excerpt { part: &text[..16] };
    println!("{}", e.part);
}
error[E0106]: missing lifetime specifier
 --> main.rs:2:11
  |
2 |     part: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct Excerpt<'a> {
2 ~     part: &'a str,
  |
Why the compiler said that

Elision only applies to function signatures. In a struct definition every reference must name its lifetime explicitly.

The fix

Add a lifetime parameter to the struct and use it on the field.

rust
struct Excerpt<'a> {
    part: &'a str,
}
rustmain.rs
struct Excerpt<'a> {
    part: &'a str,
}

impl<'a> Excerpt<'a> {
    fn word_count(&self) -> usize {
        self.part.split_whitespace().count()
    }

    // Rule 3: the returned &str borrows from self
    fn first(&self) -> &str {
        self.part.split(' ').next().unwrap_or("")
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let e = Excerpt { part: first_sentence };
    println!("{}", e.part);
    println!("{} words, starts with {}", e.word_count(), e.first());
}
Outputcompiled & run with real Rust
Call me Ishmael
3 words, starts with Call

The impl<'a> line repeats the parameter so the methods can talk about the same 'a.

Your turn

Add a field author: &'a str and print it. Both fields share one lifetime, which is the simple choice unless they borrow from different places.

In real code, most structs own their data
Reference-holding structs appear in parsers, iterators and zero-copy views, where avoiding allocations matters. For application types (a User, an Order) store String and Vec. A struct with a lifetime parameter spreads that parameter to every type that contains it, which gets noisy fast.
06

The 'static lifetime

'static means "valid for the whole run of the program". Every string literal is a &'static str, because its bytes are baked into the compiled binary and never freed. Constants and static items are 'static too.

rustmain.rs
static APP_NAME: &str = "inventory";

fn level_name(level: u8) -> &'static str {
    match level {
        0 => "debug",
        1 => "info",
        2 => "warn",
        _ => "error",
    }
}

fn main() {
    let name: &'static str = level_name(2);
    println!("[{}] {}", APP_NAME, name);
    println!("[{}] {}", APP_NAME, level_name(9));
}
Outputcompiled & run with real Rust
[inventory] warn
[inventory] error

The function takes no references, so there is nothing to borrow from; returning literals is the one case where &'static str is exactly right.

Your turn

Add a level 3 => "fatal" and move the catch-all to _ after it.

Do not add 'static to silence an error
When the compiler says a value does not live long enough, sticking 'static on the signature just moves the error: now the caller must supply data that lives forever, which a String built at runtime never does. The real fix is almost always to return an owned value or to restructure who owns what.

You will also see T: 'static as a bound, for example on std::thread::spawn. That means "T contains no borrowed data that could expire", so an owned String or Vec satisfies it. It does not mean the value lives forever. Module 10 shows why threads need this.

07

Lifetimes cheat sheet and interview angle

Lifetime
The region of code during which a reference is valid. Checked at compile time, costs nothing at runtime.
Dangling reference
A reference to memory that has already been freed. Rust rejects any program that could create one.
Lifetime parameter ('a)
A generic name for a lifetime, declared like <'a>, used to relate the lifetimes of inputs, outputs and struct fields.
Lifetime elision
Three rules the compiler uses to fill in lifetimes in function signatures so you do not have to.
'static
The lifetime of the whole program. String literals have it. As a bound (T: 'static) it means the type holds no short-lived borrows.
Borrow checker
The part of rustc that proves every reference is used only while its owner is alive and that mutable borrows are exclusive.
Mid-levelDo lifetime annotations make a value live longer?

No. Annotations are descriptive, not prescriptive. They tell the compiler how the lifetimes of inputs and outputs relate, so it can check each call site. How long a value actually lives is still decided by its owner and its scope. If the relationship you declared cannot be satisfied, you get a compile error; nothing is extended.

What they are really testing: Whether the candidate understands annotations as constraints checked by the compiler rather than a memory-management instruction.

JuniorWhy does fn longest(x: &str, y: &str) -> &str fail to compile while fn first(x: &str) -> &str compiles?

With one input reference, elision rule 2 gives the output the same lifetime as that input. With two input references and no &self, no elision rule applies, so the compiler does not know whether the result borrows from x or y and reports E0106. Adding <'a> to both inputs and the output fixes it.

What they are really testing: Knowledge of the elision rules and the ability to read E0106.

Frequently asked questions

Do Rust lifetimes have a runtime cost?
No. Lifetimes exist only at compile time. The borrow checker uses them to prove references are valid and then they are erased; the compiled program has no lifetime tracking, reference counting or garbage collector.
When should I write lifetime annotations in Rust?
Only when the compiler asks, which is usually E0106: a function returns a reference and has more than one reference input (and no &self), or a struct stores a reference. Everywhere else the elision rules fill them in for you.
What does 'static mean in Rust?
As a reference lifetime (&'static str) it means the data lives for the entire program, like a string literal. As a trait bound (T: 'static) it means the type holds no borrowed data that could expire, which any fully owned type such as String satisfies.

Finish the Rust handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.