Skip to content

The Rust Engineering Handbook

Appendix C — Lifetime Pattern Catalog

Read lifetime signatures as relationship contracts, select the narrowest coupling, and recognize when ownership is the better API.

Consider an API review with this proposed repair:

fn field<'a>(document: &'a str, name: &'a str) -> &'a str

The code compiles. The signature is still suspicious. It says the returned field may borrow from either document or name, and it limits the result to the lifetime they can share. If the implementation only returns a view into document, coupling name to the same lifetime rejects callers for no semantic reason.

Lifetimes in signatures are not estimates of elapsed time. They name relationships the caller and implementation must honor. Read each lifetime parameter as an equality or outlives constraint over borrows; then ask whether the data flow requires that constraint. The best signature is not the one with the fewest apostrophes. It is the one that exposes every real borrowing relationship and invents none.

This catalog starts from that reading job. It covers the reusable patterns behind input borrows, returned views, callbacks, reference-bearing types, lending iterators, and 'static bounds. Use it after the ownership tables in Appendix B have established that a borrowed API is appropriate at all.

Four lifetime patterns compare a borrow contained by an owner interval, independent borrowed inputs producing an owned result, a returned view tied to one input, and a higher-ranked callback accepting a fresh short borrow on every invocation.
Read the solid arrows as data relationships and the repeated callback frames as quantification: a named lifetime relates positions, while for<'a> requires the callback to accept each fresh borrow chosen by its caller.

The four panels distinguish duration from quantification. A named lifetime relates positions in a type. for<'a> moves the quantifier: the callback must work for every lifetime chosen by its caller.

Read the signature before naming lifetimes

Start with three questions:

  1. Which owner contains the bytes or value behind every returned reference?
  2. Which inputs can vary independently at a call site?
  3. Who chooses each lifetime: the function’s caller, the function implementation, or a callback’s caller?

Lifetime elision answers common cases without changing their semantics. In fn head(input: &str) -> &str, the single input reference supplies the output lifetime. Methods also give &self or &mut self a privileged elision rule for outputs. Elision is syntax reduction, not weaker coupling.

When two or more input references exist, an output reference generally needs an explicit relationship. Name only positions that participate in that relationship:

fn field<'doc>(document: &'doc str, name: &str) -> Option<&'doc str> {
    document
        .split(';')
        .find_map(|entry| entry.split_once('='))
        .filter(|(key, _)| *key == name)
        .map(|(_, value)| value)
}

Here name is borrowed for the call but independent of the result. A reviewer can see that retaining the result keeps document borrowed, not name.

Pattern: borrow from exactly one input

Use this pattern when a result is a view into a specific caller-owned input:

fn before_colon(input: &str) -> &str {
    input.split_once(':').map_or(input, |(head, _)| head)
}

The elided form is usually clearest. Its expanded relationship is:

fn before_colon<'a>(input: &'a str) -> &'a str

The contract does not promise that the result remains valid for all of 'a; a caller may use it for any shorter valid period. It promises that the result cannot be used beyond the relevant borrow of input. Mutation requiring exclusive access must wait until the view’s last use.

This is a good fit for slices, parsed fields, lookup results, and projections through a stable owner. It is a poor fit when the implementation may later need to synthesize data, normalize it into a new allocation, or switch storage backends. An owned result or Cow<'a, T> may provide the intended compatibility boundary, but that decision belongs to the conversion matrix in Appendix E.

Rejected evidence matters. A returned view cannot escape a local owner:

let view = {
    let line = String::from("host: west");
    before_colon(&line)
};
println!("{view}");

Returning String would repair this only if the conceptual result is independent. Moving the owner outward is the better repair when it remains authoritative storage.

Pattern: independent inputs stay independent

Inputs used only during a call usually do not need named lifetimes:

fn same_width(left: &str, right: &str) -> bool {
    left.len() == right.len()
}

Even when a function returns a view from one input, unrelated inputs should remain unrelated:

fn locate<'haystack>(haystack: &'haystack str, needle: &str) -> Option<&'haystack str> {
    haystack.find(needle).map(|start| &haystack[start..start + needle.len()])
}

By contrast, a selector that may return either input needs a relationship broad enough for both:

fn choose<'a>(left: &'a str, right: &'a str, left_wins: bool) -> &'a str {
    if left_wins { left } else { right }
}

At a particular call, 'a becomes a region valid for both candidate borrows and the returned view. Informally, that is constrained by the shorter usable overlap. This does not make the owners’ actual scopes equal.

An enum can preserve provenance more precisely when the caller needs to know which side won:

enum Selected<'l, 'r> {
    Left(&'l str),
    Right(&'r str),
}

That adds handling complexity but avoids collapsing two independent relationships. Use it when provenance affects authorization, mutation, retention, or diagnostics—not merely to demonstrate lifetime syntax.

Pattern: returned views and nested owners

A reference may be projected through several owners while retaining one externally visible relation:

struct Catalog {
    names: Vec<String>,
}

impl Catalog {
    fn name(&self, index: usize) -> Option<&str> {
        self.names.get(index).map(String::as_str)
    }
}

The returned &str is tied to the borrow of Catalog. Internal Vec and String ownership is hidden. This is powerful but creates an API constraint: callers holding a name view prevent operations needing &mut self, and changing from internally stored strings to computed names may require allocation or a new return type.

Do not expose a reference merely because the current implementation has one available. Borrowed output is a compatibility promise about coupling. Owned output pays transfer or allocation costs but permits independent retention and implementation changes. An index or domain handle is another alternative when identity, not a continuous borrow, is the useful contract.

Pattern: structs containing references

A reference-bearing struct says that instances cannot outlive borrowed backing storage:

#[derive(Debug)]
struct Header<'a> {
    name: &'a str,
    value: &'a str,
}

impl<'a> Header<'a> {
    fn parse(line: &'a str) -> Option<Self> {
        let (name, value) = line.split_once(':')?;
        Some(Self { name, value: value.trim() })
    }
}

This is appropriate for request-scoped parsers, packet views, compiler syntax trees over stable source text, and other arenas where one owner clearly dominates all views. It can avoid allocation and retain exact source slices.

The cost is architectural. The lifetime becomes part of every containing type and API. The owner must remain stable and usable for at least as long as every view, mutation may be restricted, and self-referential ownership remains difficult. A struct cannot ordinarily own a String and safely store a reference into that same movable String using ordinary references.

Credible alternatives include:

  • own each field, accepting allocation or copying;
  • store ranges or offsets and resolve them through an external owner;
  • use an arena whose lifetime intentionally dominates the view graph;
  • separate an owning storage type from a short-lived view type;
  • use a specialized pinned abstraction only when its invariants and projection rules are justified and audited.

Adding 'static does not solve self-reference. It instead asks for data that contains no non-'static borrowed references.

Pattern: callbacks over any short borrow

Compare these contracts:

fn visit_one<'a>(value: &'a str, callback: impl FnOnce(&'a str))

fn visit_words(input: &str, callback: impl for<'a> FnMut(&'a str))

In the first, the outer caller chooses 'a, and the callback accepts that particular borrow. In the second, the iterator-like function repeatedly chooses fresh short lifetimes. The callback must accept &str for every such choice. This is a higher-ranked trait bound (HRTB).

That universal quantification prevents the callback type from demanding one specific external borrow lifetime. It is useful for scoped visitors, parsers, accessors, and APIs that lend a reference only for the duration of each call:

fn visit_words(input: &str, mut visitor: impl for<'a> FnMut(&'a str)) {
    for word in input.split_whitespace() {
        visitor(word);
    }
}

The callback can copy, hash, count, log, or transform each word. It cannot store an arbitrary invocation’s &str into longer-lived state. If retention is required, the API must transfer ownership, expose storage with an appropriate outer lifetime, or let the callback produce an owned result.

Do not add an HRTB because it looks more general. Add it when the callee must invoke an operation over borrows whose lifetimes it chooses. Closure bounds without explicit for<'a> often already receive the needed late-bound behavior; spell the quantifier when it explains or enforces the contract.

Pattern: iterator lending and its stable limits

The standard Iterator trait has one Item type for an implementation:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

Item cannot vary with the lifetime of each &mut self borrow. Iterators can yield references tied to an owner lifetime already carried by the iterator—slice::Iter<'a, T> yields &'a T—but the general trait does not express an item borrowed from the iterator itself for only the current call.

Generic associated types make a lending trait expressible on stable Rust:

trait Lend {
    type Item<'a>
    where
        Self: 'a;

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

An implementation can set Item<'a> = &'a mut Record, tying each item to that invocation’s borrow. While the item is usable, another call requiring &mut self is rejected. The relationship prevents retaining one lent item and advancing again as if the iterator were independently available.

This mechanism has practical limits. Generic associated types, higher-ranked bounds, type inference, and dyn compatibility interact in ways that can make generic adapters or erased interfaces difficult. Prefer standard Iterator when items are owned or borrow from separate stable storage. Use a lending-specific trait when borrowing from internal reusable buffers or enforcing one-at-a-time access materially changes allocation or safety. Keep the trait narrow, preserve compile tests for intended consumers, and do not present every desirable adapter as automatically available.

Pattern: understand both meanings of 'static

&'static T is a reference valid for the entire program. String literals are the usual example. A deliberately leaked allocation can also produce one, but leaking is an ownership decision, not a routine lifetime repair.

T: 'static means T contains no borrowed data whose lifetime is shorter than 'static. It does not mean the value will remain allocated forever:

fn require_static<T: 'static>(value: T) {
    drop(value); // immediate destruction is allowed
}

require_static(String::from("owned"));

Owned String, Vec<T> with suitable T, and many other ordinary values satisfy 'static because they do not borrow short-lived stack data. A move closure can satisfy a spawned thread’s 'static bound by owning its captures, even though the thread may finish moments later.

Treat a 'static bound as a prohibition on short borrowed edges at that boundary. Before adding it, ask:

  • Can the operation be scoped and joined before the owner ends?
  • Should ownership cross the boundary instead?
  • Is independent lifetime genuinely required by a registry, executor, thread, or stored callback?
  • Would requiring 'static unnecessarily exclude callers with valid borrowed data?

Box::leak, global storage, and cloning into an owned value are semantically different responses. Choose based on retention and destruction requirements, not compiler appeasement.

Failure diagnoses and design repairs

Symptom Relationship revealed First design question Repair families
missing lifetime specifier on returned reference compiler cannot infer the source input which owner supplies the result? tie to exact input; return owned data; encode provenance
borrowed value does not live long enough consumer outlasts owner/view validity should consumption finish sooner or ownership move outward? shorten use; move owner; own result
explicit lifetime makes unrelated input “not live long enough” signature over-couples independent inputs can that input use an elided independent borrow? split lifetimes; remove name; redesign selector
closure requires borrowed data to escape callback attempts retention beyond an invocation is retention part of the callback contract? return owned output; move owner; scoped storage
repeated mutable call fails while prior item is live item borrows from receiver must items coexist? finish with item; own/copy item; externalize storage
'static required at spawn/store boundary boundary may outlive current scope can the work be scoped or own captures? scoped API; ownership transfer; deliberate sharing

A compiling fix can still be a bad contract. Broad lifetime equality reduces caller freedom. Cloning hides coupling by changing the API to ownership. Leaking suppresses destruction. Replacing references with IDs introduces lookup and stale-handle rules. Each repair changes the system; record that change in review.

Applied exercises

Signature audit. For each signature, draw owner nodes, mark which positions share a named lifetime, and rewrite only if the relationship is too broad or impossible:

fn find<'a>(data: &'a str, query: &'a str) -> Option<&'a str>;
fn render<'a>(template: &'a str, values: &Values) -> String;
fn select<'a, 'b>(left: &'a str, right: &'b str) -> Selected<'a, 'b>;
fn register<F>(callback: F) where F: for<'a> Fn(&'a Event);

State who chooses every lifetime and whether the output owns or borrows. Then identify one future implementation change each borrowed return would constrain.

Parser architecture. Design a header parser in three forms: owned fields, a Header<'a> view, and offset-based fields resolved through an owner. Compare allocations, mutation restrictions, error reporting, serializability, and the ability to retain selected headers after discarding the source buffer.

Lending review. Implement a reusable line buffer that yields one borrowed line at a time. Explain why standard Iterator is insufficient if the line borrows from the iterator’s mutable buffer. Define the narrowest GAT-based trait, demonstrate that a caller cannot hold a line across the next call, and compare an owned String iterator.

The companion crate at examples/rust-engineering-handbook/appendices/lifetime-trait-lab/ contains accepted patterns and rejected doctests for escaped views and incorrectly related inputs.

Review card

  • Identify the owner behind every returned reference.
  • Mark inputs that are truly independent; do not give them one lifetime for convenience.
  • Say whether the outer caller or the callback’s caller chooses the lifetime.
  • Treat a reference-bearing struct as an architectural coupling to backing storage.
  • Confirm whether an iterator item borrows external storage or the iterator itself.
  • Translate T: 'static as “no short borrowed data,” not “lives forever.”
  • Compare borrowed output with ownership, offsets, handles, arenas, or scoped execution.
  • Preserve accepted and rejected compiler evidence for public lifetime contracts.

Appendix D continues from relationships between references to relationships between implementations. Its central question is not how long a value may be used, but which behavior a caller may select and how that behavior is dispatched.

Sources and version notes

  • The Rust Reference on lifetime bounds defines outlives constraints and higher-ranked bounds.
  • The Rust Reference on lifetime elision specifies the input and output elision rules.
  • The Rust Book lifetime chapter develops signature relationships and structs containing references.
  • The standard Iterator documentation is the authority for its associated Item and next contract.
  • Generic associated types are stable language functionality; exact compiler diagnostics, inference behavior, and supported compositions remain compiler-version-sensitive and need regression tests.
  • Examples target Rust 2024. The companion crate declares Rust 1.85 as its MSRV; the writing run records only toolchains it actually executes.