Reading Rust

Part 2 · The compile-time machine I — borrows & lifetimes

Lifetimes are regions of code, not durations of time

The single most damaging misconception in Rust. A lifetime is not how long a value lives — it is a set of places in your program, and it is inferred, not chosen.

60 minThreshold conceptrefutation textnotional machinePOEproductive failure
What this lesson is trying to break (5)
  • A lifetime is how long a value exists at runtime.
  • `&'a T` means "this reference lasts for duration 'a".
  • Annotating a lifetime tells the compiler to keep something alive longer.
  • `'a` in a signature is a specific lifetime the function author picks.
  • Lifetimes cost something at runtime.
Liminality warning

This is a threshold concept: troublesome, transformative, and irreversible. You should expect to be genuinely stuck here, and being stuck is the mechanism rather than a sign that you are failing. Budget several sessions. Do not move on because you are bored — move on when you can explain it out loud without notes.

This is the hardest lesson on the site. It is also the one that changes the most, because almost everyone carries a wrong model here for a long time — including people who write Rust professionally and get by on pattern-matching against compiler errors.

You are going to be stuck. Budget more than one sitting.

The misconception

Unlearn thiscarried over from the ordinary English word “lifetime”, and every tutorial that says “how long a reference is valid”
You think

A lifetime is a duration. &'a str means “a reference that lives for the period of time called 'a”. Longer lifetimes are bigger durations, 'static is the longest one — the whole program — and writing 'a on something tells the compiler how long to keep it alive.

Wrong here

Nothing about a lifetime is temporal, and annotations never keep anything alive. If durations were the model, fn f<'a>(x: &'a str) would have to mean some specific span — but the same function works for callers whose data lives for a microsecond and callers whose data lives forever, with no change. A duration cannot do that.

Instead

A lifetime is a region: a set of points in your program’s control-flow graph over which a reference must stay valid. Not a span of time — a set of places in the code. The compiler computes it from where the reference is actually used. You never choose one; you only describe how several of them relate.

That is not a metaphor or a simplification for teaching. It is the literal definition used by the compiler. The RFC that introduced the current borrow checker has a section titled “Lifetimes are sets of program points”.

What the compiler actually computes

Take this function and label every point:

fn main() {
    let mut v = vec![1, 2, 3];   // 1
    let r = &v[0];               // 2   ← reference created
    println!("{r}");             // 3   ← last use of r
    v.push(4);                   // 4
    println!("{:?}", v);         // 5
}

The borrow checker asks one question: at which points must r be valid?

It is created at 2 and last used at 3. So its region is the set {2, 3}. That is the lifetime. Not “about two nanoseconds” — the set containing points 2 and 3.

point │ what happens          │ is the borrow of v live here?
──────┼───────────────────────┼──────────────────────────────
  1   │ v created             │
  2   │ r = &v[0]             │  ●  region starts
  3   │ println!("{r}")       │  ●  last use — region ends
  4   │ v.push(4)             │     needs &mut v — no conflict
  5   │ println!("{:?}", v)   │

Then it checks whether anything conflicts within that set. v.push(4) needs &mut v at point 4, and 4 is not in {2, 3}, so there is no overlap and the program is accepted.

Move the println!("{r}") to after the push and the region becomes {2, 3, 4, 5}. Now point 4 is inside it, the exclusive borrow overlaps the shared one, and you get E0502.

Nothing about that reasoning involves time. It is set membership over a graph.

This is what “non-lexical lifetimes” means, by the way. Before NLL, regions were forced to be whole lexical blocks; now they are computed from actual use. The name describes what was removed, which is why it is such an unhelpful name.

Predict firstcommit in writing, then look
// A
fn a() {
    let mut v = vec![1];
    let r = &v[0];
    v.push(2);
    println!("{r}");
}

// B
fn b() {
    let mut v = vec![1];
    let r = &v[0];
    println!("{r}");
    v.push(2);
}

// C
fn c() {
    let mut v = vec![1];
    {
        let r = &v[0];
        println!("{r}");
    }
    v.push(2);
}

Annotations describe relationships, not durations

Now the part that trips up people who have got everything so far.

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

The usual reading is: “both arguments must have the same lifetime, and the result has that lifetime too.” That is wrong in a way that matters.

'a is a variable, universally quantified. The signature says: for any region 'a the caller cares to pick, if both inputs are valid across 'a, the output is valid across 'a too. The author is not naming a duration. They are stating a relationship between inputs and output.

The caller instantiates it. And because references are covariant in their lifetime — a reference valid over a big region can be used where one valid over a smaller region is wanted — you can pass two references with completely different scopes. Both get shortened to a common region.

Predict firstcommit in writing, then look
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

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

Lifetimes cost nothing

Worth stating flatly, because it comes up in discussions and people get it wrong in both directions.

Lifetimes are erased before code generation. There is no runtime representation of 'a. No check happens while your program runs. Two functions that differ only in lifetimes compile to byte-identical machine code.

Contrast this with the other half of the type system: generics (3.3) do affect codegen, because monomorphisation generates a copy per type. Lifetimes never do. Same syntax position in the angle brackets, completely different consequences — and being precise about that difference is a good marker of understanding.

Two relations you will read constantly

Varies: what is on the left of the colon

WrittenRead asMeans
'a: 'b"'a outlives 'b"Region 'a contains region 'b. Anywhere 'b is valid, 'a is too.
T: 'a"T outlives 'a"Every reference inside T is valid across all of 'a, so a T may be held there.
T: 'static"T contains no borrowed data"Either owned data, or references to things valid for the whole program. Not "lives forever".

The colon is a subset relation on sets of program points. Reading it as 'outlives' is idiomatic, but the set reading is the one that makes errors decodable.

That third row is worth dwelling on, because 'static is the most misread thing in the language and it has two different meanings depending on where it appears:

  • &'static T — a reference valid for the entire program. String literals, and things you deliberately leaked.
  • T: 'static — a bound, meaning T has no borrowed data with a shorter region. String satisfies it. i32 satisfies it. &'a str does not, unless 'a is 'static.

When tokio::spawn requires F: Future + Send + 'static, it is the second meaning: your future must not borrow anything from the calling stack frame, because the task may outlive it. It is emphatically not “your future must live for the whole program”.

Getting this wrong is extremely common, and getting it right is a reliable signal in conversation. We come back to it in 2.4.

Why the error messages use time words

One loose end that causes real confusion.

If lifetimes are regions, why does the compiler say s2 does not live long enough”? That is duration language, and it teaches the wrong model to every beginner who reads it.

The honest answer: the error messages describe the consequence in human terms rather than the mechanism. “Does not live long enough” means “there is no valid region assignment”, but that sentence would help nobody. The messages are aimed at getting you unstuck, not at teaching the theory.

So: trust the error’s location, and translate its wording yourself.

The one that matterswrite it — thinking it does not count

Explain to a colleague, without using the words “time”, “long”, “duration” or “live”, what fn first<'a>(items: &'a [String]) -> &'a str guarantees.

Then answer: what would change if the signature were fn first<'a, 'b>(items: &'a [String]) -> &'b str? Is that even implementable?

(Second part is hard. Attempt it before deciding.)

What you should now be able to say

  • A lifetime is a region — a set of program points where a reference must be valid. It is inferred from use.
  • 'a in a signature is a universally quantified variable the caller instantiates. Annotations state relationships, never durations.
  • Adding annotations cannot make a value live longer. The fixes are: shrink the region, extend the value’s scope, or stop borrowing.
  • Lifetimes are erased — zero runtime cost, unlike generics.
  • 'a: 'b is set containment. T: 'static means no borrowed data, not lives forever.
  • The compiler’s time-flavoured wording describes the symptom, not the mechanism.

Next: 2.4 — Reading lifetime annotations, which is the practical half: elision, '_, and the annotations you will actually meet in real signatures.

In the wildgo read the real thing now, while it is fresh
  • NLL RFC 2094 — the definitionRead the section titled "Lifetimes are sets of program points". That heading is this entire lesson, stated by the people who designed it. Skip the formalism; the framing is the payload.
  • std::str::from_utf8 — a signature to decodeLook at the elided lifetimes and ask what relationship they encode between input and output. Then read the "Errors" section and notice the returned &str borrows from the input slice — the signature is what makes that safe.
Scheduled for recall5 items added to your review queue

These are not shown to you now on purpose. They will surface in Review tomorrow, mixed in among items from other parts of the course — not grouped by topic. Sorting out which idea applies is the part that transfers.