---
title: 'The Lifetime Concept'
source: 'https://academia.sh/en/courses/ownership/lifetime-concept'
course: 'Ownership and the Memory Model'
language: en
updated: '2026-09-15T04:58:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Defines a lifetime as the source range over which a reference is valid'
    - "Runs a slice of the measurement core to show that a reference's usage range stays within its owner's life"
    - "Runs a measurement to show that two independent references' validity ranges need not be nested"
---

# The Lifetime Concept

Establishes that the source range over which a reference is valid is not a runtime object, only a range the compiler checks. The range shown to be checked in the Borrow Checker lesson is given a name here, and it is measured that two independent references' ranges need not be nested.

The Borrow Checker lesson showed that it was checking something, but it never named that
something: the checker was controlling a range tied to how long a reference could be used.
This lesson gives that range a name and applies the course's question ("where does this
value's life end") to a reference: where does a reference's **own** validity range start and
end, and where is that range read from?

The question asked by the Go curriculum's Concurrency course was different — there, what was
measured was when a running unit would stop, a stop triggered by an outside decision (a
channel closing, a context being canceled). What is measured here is not a unit of work; it
is the range in the source over which a reference can be used. The Borrowing lesson had
already shown this range's **consequence**: when a reference's scope ended, it did not end
anything itself — the value it borrowed was still alive. This lesson goes down to that
consequence's **cause** — whether the reference itself has a named range of its own, and if
so, how that range is determined.

## A Range's Name

In Rust, a **lifetime** is the name of the source range over which a reference is considered
valid. This range has two ends: the point where the reference is created and the point where
it is last used. A lifetime defines the distance between these two points, and the compiler
makes sure a reference is never used outside that range. Without this check, a reference
could still be held after the value it pointed to had ended, and a read could be attempted
through it; the result would be an undefined read into memory that was no longer valid. Rust
never leaves this to runtime: a range violation is caught at compile time, before the program
has run even once.

There is one crucial distinction to establish here: a lifetime is not a **runtime** object.
Nowhere during the program's run is there a counter, a timestamp, or a record stored under
the name "this reference's lifetime." The `Trace` type measured in the previous lesson
implemented the `Drop` trait and wrote to the ledger when a value ended — but that was the
end of an **owned value**. A reference (`&Trace`) does not implement `Drop`; the reference
itself never drops into the ledger, because its "ending" is not an event that happens at
runtime but a boundary verified at compile time. A lifetime is **read** from the block
structure in the source; once the run is over, no trace of it remains.

**LI1.** A lifetime is not a value; it is the name of an inference the compiler carries out
over the source — it is stored nowhere while the program itself is running. It fits none of
the four end classes measured in the course's earlier lessons: a lifetime is not the end of a
value but the range over which a reference **is considered valid**; what is measured is not
an event but a source range.

Once established, this boundary will be used again and again through the rest of the course:
every new mechanism (explicit annotation, elision rule, struct field, heap-allocated single
ownership, shared ownership) will approach the same question from a different angle, but all
of them will rest on the same foundation — the fact that a reference cannot outlive its
owner.

When it needs to, Rust itself writes a lifetime down as a short name starting with an
apostrophe (`'a`, `'b`); this naming is not a value, only a way of telling the compiler and
the reader "this reference and that reference are tied to the same range." When these names
need to be written and what they express is the subject of the next lesson; the only thing
that matters here is that a range having a name is not what makes that range exist at
runtime.

## The Difference Between Scope and Lifetime

The Programming Fundamentals course established the concept of scope: the block in which a
name appears, and the point past which it becomes unreachable once that block ends. A
lifetime is related to this concept but is not the same thing. Scope is a syntactic
boundary — it is drawn by a pair of `{` and `}` in the source and covers an entire block. A
lifetime, by contrast, is a **usage** range: the range over which a reference actually has to
be valid is not the whole block it sits in, only the segment from the point it is created to
the point it is last used.

The measurement below makes this distinction concrete: the references `r1`, `r2`, and `r3`
are created inside a scope but do not fill that scope **entirely** — after the line where
they are last used, several more lines of code still run before the scope closes, and those
lines never touch the references again. A reference's validity range runs from where it is
created to where it is last used; the rest of the scope still matters for the three values'
**ownership** (all three stay alive until the scope closes), but it no longer matters for the
references. This is why a lifetime is named as a separate concept: scope alone is not enough
to say how long a reference stays safe.

This difference cuts both ways: although a reference's range does not have to **fill the
scope entirely**, it also cannot **spill outside it**. A reference can never keep being used
after it has left the block it belongs to — a validity range is a subset of scope, not a line
independent of scope and fully detached from it. The explicit lifetime annotations seen in
later lessons do not break this relationship either; they only make a bond between several
references' ranges visible in the source when such a bond exists.

## Measurement: A Reference's Range Stays Within Its Owner's Life

Part A of the measurement core in the Stack and Heap lesson had shown that three values
(`scope-1`, `scope-2`, `scope-3`) are set up in the same scope and end in the reverse order
of their declaration once the scope closes. The same slice is run again here, this time with
references taken: a reference is taken to each of the three values while still inside the
scope, a read is made through those references, and the ledger is checked at the moment of
that read.

```rust
// reference_range.rs — a slice of life.rs's part A: three values, read through references.

use std::cell::RefCell;
use std::rc::Rc;

type Ledger = Rc<RefCell<Vec<String>>>;

struct Trace {
    name: String,
    ledger: Ledger,
}

impl Trace {
    fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace { name: name.to_string(), ledger: Rc::clone(ledger) }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

fn read(ledger: &Ledger) -> Vec<String> {
    ledger.borrow().clone()
}

fn newly_added(ledger: &Ledger, before: usize) -> String {
    let all = read(ledger);
    all[before..].join(", ")
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));

    let a0 = read(&ledger).len();
    {
        let scope_1 = Trace::new("scope-1", &ledger);
        let scope_2 = Trace::new("scope-2", &ledger);
        let scope_3 = Trace::new("scope-3", &ledger);

        let r1 = &scope_1;
        let r2 = &scope_2;
        let r3 = &scope_3;
        println!("read through references -> {}, {}, {}", r1.name, r2.name, r3.name);
        println!("ledger at read time      -> [{}]", newly_added(&ledger, a0));

        // The references' last use was on the line above; none of them appear below, but
        // the three values keep living as owners until the scope itself closes.
        let still_empty = newly_added(&ledger, a0).is_empty();
        println!("ledger still empty while scope open -> {}", still_empty);
    }
    println!("once the scope closes   -> {}", newly_added(&ledger, a0));
}
```

```
read through references -> scope-1, scope-2, scope-3
ledger at read time      -> []
ledger still empty while scope open -> true
once the scope closes   -> scope-3, scope-2, scope-1
```

The references `r1`, `r2`, and `r3` are created and read at the same point, and the ledger is
still empty at that moment — all three values are still alive, because the scope has not yet
closed. The references' last use is on that very same line: they are never used again.
The line that follows touches none of the references and confirms the ledger is still empty —
all three values keep living as owners, even after the references have finished their work,
right up to the scope's own end. Once the scope closes they end in the reverse order of their
declaration — the rule established in the Stack and Heap lesson holds here too. The
references' own validity range sits **inside** the life of these three bindings; it does not
spill outside it, but it does not have to fill the entire life of the bindings either.

**LI2.** A reference's validity range is a sub-range of the life of the value it refers to —
a reference can never be used past the point where its owner ends. Code that crosses this
boundary is rejected by the compiler; the concrete form of that rejection was shown running
in the Borrow Checker lesson.

Suppose the measurement above tried to move a reference like `r1` outside the inner scope:
say `r1` were assigned to a variable defined in the outer scope, or returned from a function.
That code would not compile, because `scope_1`, the value `r1` is tied to, ends when the
inner scope closes, and `r1` would then be held somewhere that kept living past that point —
a reference outliving its own owner is exactly what a lifetime does not allow. The run above
was accepted because all three references stayed inside their own inner scope; the difference
between what is accepted and what is rejected is whether the reference's point of use falls
inside or outside its owner's scope.

## Limiting Measurement: Ranges Need Not Be Nested

Textbooks often draw lifetimes as nested boxes, and that picture can leave a false
impression: as if every reference's range had to nest inside some outer range. In reality,
two independent references' ranges need not have any relationship to each other at all — they
do not even need to overlap.

```rust
// independent_ranges.rs — two references' validity ranges never intersect.

use std::cell::RefCell;
use std::rc::Rc;

type Ledger = Rc<RefCell<Vec<String>>>;

struct Trace {
    name: String,
    ledger: Ledger,
}

impl Trace {
    fn new(name: &str, ledger: &Ledger) -> Trace {
        Trace { name: name.to_string(), ledger: Rc::clone(ledger) }
    }
}

impl Drop for Trace {
    fn drop(&mut self) {
        self.ledger.borrow_mut().push(self.name.clone());
    }
}

fn read(ledger: &Ledger) -> Vec<String> {
    ledger.borrow().clone()
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));

    let trace_a = Trace::new("independent-a", &ledger);
    let trace_b = Trace::new("independent-b", &ledger);

    {
        let r = &trace_a;
        println!("read through a -> {}", r.name);
    }
    println!("ledger after reading a -> [{}]", read(&ledger).join(", "));

    {
        let r = &trace_b;
        println!("read through b -> {}", r.name);
    }
    println!("ledger after reading b -> [{}]", read(&ledger).join(", "));
}
```

```
read through a -> independent-a
ledger after reading a -> []
read through b -> independent-b
ledger after reading b -> []
```

`trace_a` and `trace_b` live together in the same outer scope and both end only at the very
end of the program — the ledger stays empty after both reads, because neither has ended yet.
But the picture is different for the references: the reference taken to `trace_a` is created
in its own inner block and used there, and its validity ends once that block closes; the
reference taken to `trace_b` is created afterward, in a completely separate block. The two
references' validity ranges never overlap in time at all — one closes fully before the other
opens. The compiler never has to compare these two ranges at any point, because no
relationship was ever established between them.

**LI3.** Two references' validity ranges do not have to be nested or to intersect; ranges
that are fully independent of each other, even ones that never overlap in time, can sit side
by side in the same program. As long as a function signature does not establish a
relationship between two references, those two ranges stay independent for the compiler
too — when that relationship needs to be established is the subject of the next lesson.

The practical upshot of this independence is that dozens of references can be alive at once
in a program, and most of them have no relationship to each other at all — each exists in its
own independent range, tied to its own owner. The compiler only compares ranges that are
genuinely related: when one reference depends on another reference's data, or when a function
establishes a bond between two references. Two unrelated references never enter that
comparison, just as in the `trace_a` and `trace_b` example above. This also explains why the
explicit syntax seen in the next lesson only appears **when it is needed**: if there is no
relationship, there is nothing to write.

## Reading the Range from the Source

The two measurements above share one feature: neither of them printed a lifetime directly.
The only thing printed was when the owned values ended — a reference's own range never
appeared in any output, because there is no runtime counterpart for it to appear as. The only
way to learn a reference's range is to read the source: `r1`'s range runs from the line
`let r1 = &scope_1;` to the print call where `r1.name` is used; there is no separate tool to
run either before or after that.

This continues the pattern established in the course's earlier lessons (the
expression/statement/item classification, the four end classes): every measurement produces a
conclusion drawn from the source's own structure, not a number or an ordered record left over
once the run is done. For a lifetime, this means information that never appears in the output
but is always visible in the source.

The same reading method holds in the second measurement: to find the range of the reference
on `trace_a`, there is no need to look at the ledger or at any other output — in the source,
the inner block from `let r = &trace_a;` to the line where `r.name` is used is the whole of
that range. The range on `trace_b` is bounded the same way, by its own inner block. The lines
that fall between the two references' ranges — after the first block closes, before the
second block opens — belong to neither range, and that too is understood only by reading the
source. No runner or extra tool contributes anything to this conclusion; the measurement core
exists here only to make concrete a reading that was already correct.

## Summary

- A lifetime is the name of the source range over which a reference is considered valid, and
  it is not a runtime object — only an inference the compiler carries out over the source.
- A reference does not implement `Drop` and never drops into the ledger; only owned values
  do; an owned value's life ends **at scope end**.
- A reference's validity range is a sub-range of the life of the value it refers to, and this
  range cannot spill past its owner's end.
- Two independent references' validity ranges need not be nested or intersect.
- A reference's range never appears in any output; its only source is where in the source
  code the reference is created and last used.

## Next Step

This lesson showed that a reference's range stays within its owner's life, that this range
leaves no trace at runtime, and that two independent ranges may enter no relationship with
each other at all. But some functions have to establish exactly that kind of relationship:
when a function is written that takes multiple references and returns one of them, which
input the returned reference is tied to cannot be inferred from the source alone, and the
compiler does not accept that ambiguity. The next lesson measures how the range is written
explicitly in that case, what information this writing adds to the source, and what happens
when what is written does not actually hold.
