---
title: 'Explicit Lifetime Annotations'
source: 'https://academia.sh/en/courses/ownership/explicit-lifetime-annotations'
course: 'Ownership and the Memory Model'
language: en
updated: '2026-09-15T04:58:41+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Determines when an explicit lifetime annotation is required in a function signature'
    - 'Runs code to show which input the annotation ties the returned reference to'
    - 'Shows that an annotation that cannot be satisfied is rejected, and is accepted once made satisfiable'
---

# Explicit Lifetime Annotations

When a function takes multiple references and returns one of them, which input the returned reference is tied to cannot be inferred from the source alone, and an explicit lifetime annotation is required. It is measured that the annotation neither extends nor shortens a reference's life, only writes an existing relationship into the source, and that the compiler validates this writing as a request.

The previous lesson showed that two independent references' ranges can sit side by side
without entering any relationship — most references live unaware of each other, and the
compiler builds no bond between them. But when a function takes multiple references and
returns one of them, independence is no longer enough: which input the returned reference
comes from, which input's life it is tied to, cannot be inferred from the source alone. This
lesson measures the explicit syntax that steps in right at this point — the lifetime
annotation: when it is needed, what information it adds, and how the compiler treats it as a
promise.

What the Go curriculum's Concurrency course measured does not apply here either: there, a
running unit's stopping was triggered by an outside decision; what is measured here is a
relationship written into a source's own signature. None of the four end classes established
in the Stack and Heap lesson operate here either; the annotation does not write down the end
of a value, but a relationship between two references.

Writing this relationship is not optional, it is mandatory: the compiler rejects a signature
that stays ambiguous without making any assumption. A human reader looking at a two-reference
function might guess "probably the first parameter is returned," but the compiler does not
guess — either the relationship is written explicitly or the function does not compile at
all. This repeats a principle the course established earlier: the compiler's guarantee rests
on information written in the source, not on a probable correct reading.

## When It Needs to Be Written

If a function takes a single reference and returns a reference, which input the returned
reference is tied to is obvious: the one input it took. But in a function that takes two
references and returns one, that bond becomes ambiguous — the compiler cannot tell from the
type in the source alone whether the returned reference is tied to the first input, the
second, or both. It is exactly at this moment of ambiguity that a lifetime annotation is
needed: a short name written into the function signature (such as `'a`) says which input's
life is carried into the returned reference.

This ambiguity is not only about the number of inputs, it is also about the returned value
being a reference. If a function takes two references and returns a number or a freshly built
value, no annotation is needed at all, because the returned value's life is not tied to any
input — it exists on its own. The annotation only comes into play when the returned value
**is also a reference** and which input that reference comes from cannot be read from the
type. The measurement below sets up exactly this situation: a function that takes two `Trace`
references but returns a string slice derived from only one of them.

```rust
// two_source_return.rs — the returned reference is tied only to the input carrying 'a.

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()
}

// 'a is established only between primary and the returned value; other does not enter this bond.
fn primary_name<'a>(primary: &'a Trace, other: &Trace) -> &'a str {
    println!("other is visible too -> {}", other.name);
    &primary.name
}

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

    let outside = Trace::new("lives-outside", &ledger);
    let result;
    {
        let inside = Trace::new("lives-inside", &ledger);
        result = primary_name(&outside, &inside);
        println!("result inside the call -> {}", result);
    }
    println!("ledger once inner scope closes -> [{}]", read(&ledger).join(", "));
    println!("result still usable -> {}", result);
}
```

```
other is visible too -> lives-inside
result inside the call -> lives-outside
ledger once inner scope closes -> [lives-inside]
result still usable -> lives-outside
```

`primary_name` takes two references, but the annotation is established only between
`primary` and the returned value: both carry the same `'a`, `other` does not. This is a
situation where the two sources' lives **diverge** — `outside` lives in the outer scope,
`inside` lives in the inner scope, and their lives are of different lengths. The measurement
confirms this: even after the inner scope closes and `lives-inside` drops into the ledger,
`result` is still usable, because `result` is tied only to `outside`'s life — not to
`other`'s (that is, `inside`'s) life at all.

**LI4.** An explicit lifetime annotation writes a relationship that **already exists**
between two references into the source; it does not create the relationship, it makes an
existing one visible. In `primary_name`, this relationship holds only between `primary` and
the returned value; `other` stays outside the annotation even though it is used in the
function's body.

In the measurement, `result` staying usable after `other` (tied to `inside`) has vanished is
not a coincidence; it is a consequence stated directly by the signature. `outside` was set up
before `inside` and is still standing until the inner scope closes; because `result` is also
tied to `outside`'s life, it stays valid even once `inside` ends. Had the function returned
`other` instead, that same call would have produced a reference that could no longer be used
once `inside`'s life ended — which is exactly why the signature chooses `primary`, not
`other`.

There is one more point worth noting here: writing `'a` neither extends nor shortens
`outside`'s life. `outside` was already in the outer scope, set up before `inside`, and would
have lived until the outer scope's end regardless — that would not have changed without the
annotation. The annotation's only job is to tell the compiler and the reader **which** life
the returned reference is tied to; it does not change any life itself.

## What the Annotation Covers and Does Not Cover

In `primary_name`'s signature, `'a` appears only between `primary` and the returned value;
the `other` parameter has its own separate, unnamed life, and that life has no relationship
with `'a` at all. This shows that two references being present together in the **same**
function call does not mean a relationship exists between them — a relationship exists only
to the extent it is **written** in the signature. `other`'s life could be shorter than
`primary`'s or longer; the function does not care either way, because the body only uses
`other` to read it, not to return it.

This shows that it is a virtue for an annotation to say as little as possible in a signature.
We were not required to give the `other` parameter a name — if its own life is never used
anywhere, the compiler treats it, on its own, as a separate and unnamed range. A name is
needed only when a genuinely shared relationship exists; if there is no relationship, there
is no name either.

This distinction rests on the same root as the previous lesson's limiting measurement: as
long as a function signature does not establish a bond between two references, those two
references' ranges stay independent for the compiler. Here, `'a` tied only `primary` and the
returned value together; it left `other` outside that bond, and `other` stayed in its own
independent range.

A signature can also carry more than one named life instead of a single `'a` — each
establishes its own relationship. The function below takes two references and returns both of
them inside a tuple; `'a` ties only the first part, `'b` ties only the second, and no
relationship is established between the two.

```rust
// two_named_lifetimes.rs — two separately named lifetimes, each tied to its own part of the returned tuple.

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()
}

// 'a and 'b are independent of each other: neither constrains the other.
fn return_both<'a, 'b>(x: &'a Trace, y: &'b Trace) -> (&'a str, &'b str) {
    (&x.name, &y.name)
}

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

    let outside = Trace::new("long-lived", &ledger);
    let first;
    {
        let inside = Trace::new("short-lived", &ledger);
        let (a, b) = return_both(&outside, &inside);
        first = a;
        println!("inside the inner scope -> {}, {}", first, b);
    }
    println!("ledger once inner scope closes -> [{}]", read(&ledger).join(", "));
    println!("first still usable -> {}", first);
}
```

```
inside the inner scope -> long-lived, short-lived
ledger once inner scope closes -> [short-lived]
first still usable -> long-lived
```

The tuple's second part (`b`, tied to `'b`) is only used inside the inner scope and goes out
of relevance along with that scope — `inside` drops into the ledger. The tuple's first part
(`first`, tied to `'a`) is stored in the outer scope and stays usable even after `inside` has
ended. The two named lives here really did correspond to lives of different lengths, and the
compiler accepted this because the signature asked for no equality between the two. `'a` here
and the single `'a` in `primary_name`'s signature correspond to different concrete ranges in
different functions even though they carry the same letter — a name is a label valid within
its own signature's scope, not a record shared across functions.

## Limiting Measurement: The Annotation Is Not a Promise, It Is a Request

Writing an annotation may look like giving the compiler information, but it is really a
**request**: a way of saying "let the returned reference live at least as long as `'a`." The
compiler has to verify this request, and if the request does not hold up against what the
body actually does, the code is rejected. This is what sets an annotation apart from a
documentation note: a wrong comment line does not affect compilation, but a wrong lifetime
annotation means compilation never happens at all.

If `primary_name`'s body had tried to return `other.name` — with the signature left unchanged
and only the last line changed — the compiler would not have accepted it. The signature ties
`'a` only to the `primary` parameter; `other`'s own life is not associated with `'a`, so as
far as the compiler is concerned, how long `other` will live after the call is unknown. Once
the returned value has been promised to live as long as `'a`, if the body returns a value
from a source (`other`) that does not satisfy that promise, the promise cannot be kept, and
the compiler rejects the signature; the reason for the rejection is that the `'a` annotation
was left out for `other` as well.

The compiler has no other option here: how long `primary` will live during the call is known
from the signature, but `other`'s is not. Unless the compiler can be sure the caller will
always supply, for `other`, a reference that lives at least as long as `'a`, it cannot let the
body return a value from `other` — otherwise, a caller could pass a short-lived `other` and
cause the returned reference to outlive its owner. This shows that the boundary established
in the previous lesson (a reference cannot outlive its owner) also holds across function
boundaries.

There are two ways to make this request satisfiable. The approach seen in the Ownership and
Moves lesson would work here too: if the function returned an owned value instead of a
reference (something like `other.name.clone()`), the lifetime problem would never arise,
because the returned value would no longer have a life tied to any input. This lesson does
not follow that path; it follows the second path, the one that keeps the reference: tying
both parameters to the same `'a`, so that both `primary` and `other` promise to live at least
as long as `'a`, and the body can return either one.

```rust
// unified_lifetime.rs — accepted once the request is made satisfiable: both parameters carry 'a.

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());
    }
}

// Now both are 'a: the returned value can come from either parameter.
fn other_name<'a>(_primary: &'a Trace, other: &'a Trace) -> &'a str {
    &other.name
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(Vec::new()));
    let a = Trace::new("a", &ledger);
    let b = Trace::new("b", &ledger);
    println!("{}", other_name(&a, &b));
}
```

```
b
```

`other_name` can now return `other.name`, because the signature now says both parameters
live at least as long as `'a`, and the body satisfies that promise — whichever parameter is
returned, the promise holds. The difference between `primary_name` and `other_name` is not
in behavior but in the **promise the signature covers**: the first promised only for one
parameter, the second promises for both.

**LI5.** A lifetime annotation is a request the compiler has to verify. If the body returns a
value from a source that does not keep the promise the annotation makes, the code is
rejected; once the promise is broadened to cover both sources, the same body is accepted.

Tying both parameters to the same `'a` in `other_name` is not free. When `primary_name` was
called, `other` living shorter than `primary` was not a problem at all, because the
annotation did not cover `other` in any way. When `other_name` is called, both now have to
live at least as long as `'a`; the caller cannot hand `other_name` a shorter-lived reference,
because the signature does not allow it. Broadening the promise does not make the function
more flexible — quite the opposite, it tightens the condition the caller has to meet. This
shows that an annotation determines not only "what information is added" but also "what the
added information places on the caller" — the narrowest annotation, only as much as the body
actually needs, is generally the one to prefer.

Together, these three measurements showed three faces of the annotation: `primary_name`
**wrote** a relationship, `return_both` showed that two relationships can stay
**independent**, and `other_name` showed that a relationship `primary_name` **could not
satisfy**, gets **accepted** once the annotation is broadened. In all three, the annotation
itself never changed any reference's life; it only put into writing a relationship that
already existed, or was wanted, in the source, and the compiler tested that writing for
consistency.

## Summary

- An explicit lifetime annotation is needed when a function takes multiple references and
  returns one of them, and which input the returned reference is tied to cannot be inferred
  from the source.
- The annotation writes a relationship that **already exists** between references into the
  source; it neither extends nor shortens any reference's life — the values referenced still
  end at their own **scope end**.
- A signature can carry more than one named life; each name is valid only within its own
  signature's scope, and the same name in different functions is not tied to the same range.
- The annotation is a request the compiler has to verify: the returned value must come from
  the source the annotation promised.
- A body that does not satisfy the promise is rejected; once the promise is broadened to
  cover all relevant sources, the same body is accepted, but this broadening also tightens
  the condition placed on the caller.

## Next Step

This lesson showed when an annotation **needs to be written**, what it expresses, and why it
is rejected when it is not satisfied. But most functions in the course's earlier
lessons — single-input functions, or methods that take `self` — carried no explicit
annotation at all and still compiled; in those functions, a relationship was already
established even though no annotation was written. The next lesson fills that gap: it
measures which rules the compiler uses to derive an unwritten annotation, how many signature
forms those rules cover, and which signatures fall outside them.
