Lifetimes in signatures — the modular contract
Lesson 07 ended on a limit: the checker learns what a call does only from the callee's signature, because reading bodies would turn every check into a whole-program check. So the relation between a function's inputs and its output has to be written where callers can read it. A lifetime parameter names a region, a call instantiates it afresh, the body is held to it, and three elision rules fill in the dull cases from the signature alone. The notation then reaches into structs that hold references, meets the one region the language names itself, and closes Part II with an order in which to ask why the checker said no.
New idea: a signature is a contract that splits the proof in two: the body may return no more than the signature promises, and the caller may rely on no more than it promises. Lifetime parameters are how the promise is written; elision writes the common ones for you.
Forces next: References are always valid and never null, and a moved-from name is dead, so there is nowhere to put no value here, or one of several shapes. How do we represent absence and choice without inventing null?
'static, a region, and a bound that only looks like one. (7) When the checker says no: four questions, in order.1 · A result that borrows from... which input?
Here is a function that returns one of its two arguments, and what the compiler makes of it:
fn pick(x: &str, y: &str) -> &str {
x
}
fn main() {
let a = String::from("long");
println!("{}", pick(&a, "short"));
}
error[E0106]: missing lifetime specifier
--> src/main.rs:1:30
|
1 | fn pick(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`
The body is one word long, and the compiler could read it: the result is x. It asks anyway, borrowed from x or from y?, because of what a caller does with the answer. Suppose main calls this function, drops the owner of one argument, and then uses the result. The C++ program has the same ambiguity:
const std::string& pick(const std::string& x, const std::string& y); // from x, or from y?
std::string a = "long";
const std::string* r;
{
std::string b = "short";
r = &pick(a, b); // fine if pick returns x; a dangling pointer if it returns y
}
std::cout << *r; // nothing in this file says which
In C++ the answer lives in a comment, or in the implementer's head, and a wrong guess is Lesson 01's hazard. Go's compiler removes the question by moving what the result might point at to the heap, where the collector keeps it (Go track, Lesson 03). Rust has no collector to lean on, so the answer must be written where the compiler can read it at the call. Four places are possible, and three fail:
- Read the callee's body at every call. Checking
mainwould mean analysing whatmaincalls, and what that calls. Some callees have no body to read at all: a function pointer, a closure passed in, a trait method chosen at run time. - Infer the relation from the body and remember it. The contract then becomes whatever the body happens to do. Edit the body to return
yand the verdict on every caller changes, in files the editor never opened. The Book's case for a written contract is that errors then point at the real cause instead of a use many steps away. - Assume the worst: the result borrows from every input. Always safe, and too coarse to use, as the next program shows.
The third road looks free, so it gets a failing program. A lookup returns a reference into a map; if its signature tied the result to both arguments, the key would be pinned for as long as the result lives, and a temporary key could not be used:
use std::collections::HashMap;
fn by_both<'a>(m: &'a HashMap<String, u32>, key: &'a str) -> Option<&'a u32> {
m.get(key)
}
fn by_map<'a>(m: &'a HashMap<String, u32>, key: &str) -> Option<&'a u32> {
m.get(key)
}
fn main() {
let mut m = HashMap::new();
m.insert(String::from("ada"), 36);
let ok = by_map(&m, &String::from("ada"));
let bad = by_both(&m, &String::from("ada"));
println!("{:?} {:?}", ok, bad);
}
The call through by_map is accepted: the signature says the result borrows from the map only, so the temporary key dies at the semicolon. The call through by_both is rejected with E0716, for a borrow of a key the function never returns. The relation cannot be assumed; it has to be stated, function by function, and the fourth road is the signature. The compiler's own suggestion for the first program, fn pick<'a>(x: &'a str, y: &'a str) -> &'a str, is the third road written down. The next section shows its price.
2 · A lifetime parameter names the relation
A signature that relates an output to an input needs a name for a region that can appear in both places. A lifetime parameter is that name: it is declared in angle brackets after the function's name, and written after the & of each reference it labels. Read fn pick<'a>(x: &'a str, y: &'a str) -> &'a str as a sentence about any region 'a the caller cares to choose: x is borrowed for at least 'a, so is y, and the result is borrowed for 'a. The body in the next program returns only x; the signature is the one the compiler proposed:
fn pick<'a>(x: &'a str, y: &'a str) -> &'a str {
x
}
fn main() {
let a = String::from("long");
let r;
{
let b = String::from("short");
r = pick(&a, &b);
}
println!("{}", r);
}
error[E0597]: `b` does not live long enough
--> src/main.rs:10:22
|
10 | r = pick(&a, &b);
| ^^ borrowed value does not live long enough
11 | }
| - `b` dropped here while still borrowed
12 | println!("{}", r);
| - borrow later used here
At the call, the compiler solves for 'a as Lesson 07 did for any region: the smallest set of points that covers where the result is used, which here reaches the println! on line 12. Each argument's borrow must then last at least that long. The borrow of a does; the borrow of b would have to outlast b, dropped on line 11, and Lesson 06's containment fails. The body returns x, so b is never read and the program is safe at run time. It is rejected because main cannot see the body: it has the signature, and the signature says the result may be y. One annotation changes the contract:
fn pick<'a>(x: &'a str, y: &str) -> &'a str {
x
}
fn main() {
let a = String::from("long");
let r;
{
let b = String::from("short");
r = pick(&a, &b);
}
println!("{}", r);
}
The caller is character for character the same, and so is the body. The signature now ties the result to x only, so the borrow of b ends when the call returns, and the program compiles. That is the whole mechanism: a caller's verdict is a function of the callee's signature and the caller's own code, and of nothing else. 'a is not a duration, each call picks its own region for it, and the annotation only says which positions must share one. The tied-to-both signature is not wrong, only expensive; it is the right contract for a function whose result may be either input.
&mutpick reach one 'a although their regions differ, because a shared borrow valid for a long region may stand in for one valid for a shorter region (Lesson 06). Behind &mut that permission is withdrawn. If &mut &'static str could be used as &mut &'short str, a function could store a short-lived reference into a variable its caller believes holds a 'static one. So &mut T is invariant in T: the type must match exactly, and the call below is rejected.
fn set<T>(slot: &mut T, val: T) {
*slot = val;
}
fn main() {
let mut keep: &'static str = "kept";
{
let tmp = String::from("temp");
set(&mut keep, &tmp);
}
println!("{keep}");
}
6 | let mut keep: &'static str = "kept";
| ------------ type annotation requires that `tmp` is borrowed for `'static`
The caller may rely on the signature. A promise is worth relying on only if someone checks it, and that is the other half of the contract.
3 · The body is held to the signature
Inside the body, 'a is not a region the body chooses. The caller chooses it, differently at every call, so the body must be correct for every region the signature allows and may assume only what the signature says; the rustc guide calls this a universal region. A body that returns a borrow the signature did not promise is rejected in the callee, by a check that reads no caller:
fn pick<'a>(x: &'a str, y: &str) -> &'a str {
y
}
fn main() {
let a = String::from("long");
println!("{}", pick(&a, "short"));
}
error[E0621]: explicit lifetime required in the type of `y`
--> src/main.rs:2:5
|
2 | y
| ^ lifetime `'a` required
The result was promised for 'a, which only x carries, and y has a lifetime of its own. Write the second lifetime down and the same mismatch loses its code:
fn pick<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
y
}
fn main() {
let a = String::from("long");
println!("{}", pick(&a, "short"));
}
error: lifetime may not live long enough
--> src/main.rs:2:5
|
1 | fn pick<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
| -- -- lifetime `'b` defined here
| |
| lifetime `'a` defined here
2 | y
| ^ function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'b`
|
= help: consider adding the following bound: `'b: 'a`
The message says the two halves of the contract disagree at line 2: the signature promised 'a, the body returns 'b. E0621 is the same mismatch when the offending parameter's lifetime was left unwritten. What the message cannot know is which half is wrong. Its help edits the signature: adding 'b: 'a makes every caller show that the borrow of y lasts at least as long as 'a, which may be exactly the contract you meant or a quiet narrowing of who may call. Decide from the callers: what they hold, and what they need back.This is the modularity the baton asked for: the body is checked against the signature, each call against the signature, and the two checks never meet. The signature is also where the two parties negotiate. Promise little, tied to both inputs, and the body is easy to write while every caller must keep both inputs alive. Promise a lot, tied to one input, and callers are freer while the body must return something derived from it. The widget computes every point of that negotiation for one function: five signatures, four bodies, three callers. Every cell is computed by running the mini checker's two checks.
What to try. Start on tied to x, body x, caller b, the owner of y: both halves of the cell pass. Change the body to y: the left half becomes E0621 and the right half does not move. Now slide to tied to both: every body passes on the left, and the right half is E0597 for every body, since b dies first and the signature says the result may be y. Slide to tied to nothing: the call passes whatever the body is, and on the left only the literal survives. Last, switch the caller to a, the owner of x and watch the rows tied to x and tied to y trade places on the right. The first row shows the other way to fail: with no annotations there is no contract at all, so neither half is checked. The last card, does the body change the call's verdict?, stays no on every row. That is the modularity claim measured: twelve programs per row, four bodies against three callers, and no verdict moves with the other half's code.
Each row of that table is a signature somebody had to write out in full. Two shapes of signature can be completed from their text alone, without looking at a body.
4 · Elision: the boring signatures, written for you
With one reference going in, a reference coming out can only be made from it, or be 'static; a method that returns a reference is assumed to return part of self. Writing <'a> in every such signature would be noise, so the compiler fills the lifetimes in by three rules. They read the signature text and never the body, which is what §1 demanded of any source of the relation. The Reference's rules, paraphrased:
- every elided lifetime in the parameters becomes a distinct lifetime parameter;
- if the parameters mention exactly one lifetime, it is given to every elided lifetime in the result;
- if there is a
&selfor&mut selfparameter, its lifetime is given to every elided lifetime in the result.
If none applies, the signature is refused: E0106, the error that opened this lesson. Here is what the rules produce, with the mini checker's own expansion of each signature:
| rule | you write | the compiler reads | so |
|---|---|---|---|
| 1, 2 | fn first_word(s: &str) -> &str | fn first_word<'a>(s: &'a str) -> &'a str | the result borrows from s |
| 1, 3 | fn get(&self, key: &str) -> &str | fn get<'a, 'b>(&'a self, key: &'b str) -> &'a str | the result borrows from self, not from key |
| — | fn pick(x: &str, y: &str) -> &str | refused: E0106 | two candidates and no self |
| — | fn name() -> &str | refused: E0106 | nothing to borrow from |
Rule 2 needs exactly one candidate, and rustc 1.98.1 counts them per parameter: two parameters that both say 'a are still two, and rule 2 does not apply. The message differs from the one for pick:
fn first_word(s: &str) -> &str {
s.split(' ').next().unwrap()
}
fn by_both<'a>(x: &'a str, y: &'a str) -> &str {
x
}
fn main() {}
5 | fn by_both<'a>(x: &'a str, y: &'a str) -> &str {
| ------- ------- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value with an elided lifetime, but the lifetime cannot be derived from the arguments
Elision is a guess about a contract, made from the shape of a signature. When it is right, nothing needs writing. When it is wrong, the compiler finds out as it always does, by checking the body against the signature it filled in. Rule 3 guesses that a method's result comes from self; this method returns its argument:
struct Config {
name: String,
}
impl Config {
fn pick(&self, other: &str) -> &str {
other
}
}
fn main() {}
7 | other
| ^^^^^ method was supposed to return data with lifetime `'2` but it is returning data with lifetime `'1`
|
help: consider introducing a named lifetime parameter and update trait if needed
|
6 | fn pick<'a>(&self, other: &'a str) -> &'a str {
| ++++ ++ ++
Here the error is in the callee, and the suggested edit is the right one. The rules are a convenience, not a proof: the Book calls them patterns programmed into the compiler and says more might be added; as of 2026-09 these are the three. The cost is cognitive and lands in a predictable place: the day a method hands back something that is not self's. One such place is a value that holds a borrow in a field.
5 · A struct that holds a reference
So far the borrowed thing has been an argument. A value can also hold a borrow across calls: a parser that keeps the text it is reading, the capture of a closure (Lesson 14). The relation "this borrows from that" then has to live in the type, or a value of the type could outlive what it points into. A reference field needs a lifetime parameter on the struct; the elision rules do not reach into field types:
struct Excerpt {
part: &str,
}
fn main() {}
Write struct Excerpt<'a> { part: &'a str } and Excerpt<'a> is a different type for each region. A value of it holds a loan for 'a, so by Lesson 07's rule the loan stays live as long as any name holding the value does. Methods go in an impl<'a> Excerpt<'a> block, which brings the same name into scope. Now rule 3 meets a value whose methods return pieces of the text, not pieces of the parser:
struct Words<'a> {
rest: &'a str,
}
impl<'a> Words<'a> {
fn next_word(&mut self) -> &str {
let end = self.rest.find(' ').unwrap_or(self.rest.len());
let (word, rest) = self.rest.split_at(end);
self.rest = rest.trim_start();
word
}
}
fn main() {
let text = String::from("lifetimes are contracts");
let mut words = Words { rest: &text };
let first = words.next_word();
let second = words.next_word();
println!("{first} {second}");
}
error[E0499]: cannot borrow `words` as mutable more than once at a time
--> src/main.rs:18:18
|
17 | let first = words.next_word();
| ----- first mutable borrow occurs here
18 | let second = words.next_word();
| ^^^^^ second mutable borrow occurs here
19 | println!("{first} {second}");
| ----- first borrow later used here
Nothing is wrong with the program or the body: every word is a slice of text, which no one mutates. The signature is wrong. Rule 3 tied the result to the &mut self borrow, so first holds words exclusively for as long as it lives, and the second call overlaps it by the law. The error is in the caller, far from its cause. The contract the function means is that words borrow from the text, which the struct's own lifetime names:
struct Words<'a> {
rest: &'a str,
}
impl<'a> Words<'a> {
fn next_word(&mut self) -> &'a str {
let end = self.rest.find(' ').unwrap_or(self.rest.len());
let (word, rest) = self.rest.split_at(end);
self.rest = rest.trim_start();
word
}
}
fn main() {
let text = String::from("lifetimes are contracts");
let mut words = Words { rest: &text };
let first = words.next_word();
let second = words.next_word();
println!("{first} {second}");
}
lifetimes are
One changed token moves the result from the borrow of the parser to the borrow the parser holds, and the caller may now hold both words and keep calling. The body compiled under both signatures: a borrow valid for 'a may be returned where a shorter one is promised. The shorter promise was the weaker contract, so the callee was content and the caller paid, as in §3.
Doc that owns a String and keeps a title slice of it. It can be built in place and used there. Once built it can never be moved: a move is a shallow copy of the value's bytes (Lesson 04), so a reference into the old place would point at bytes that no longer hold the value. The checker does not separate a field whose bytes move from a String whose buffer does not. The loan is on the place doc.text, and moving doc ends that place.
struct Doc<'a> {
text: String,
title: &'a str,
}
fn main() {
let mut doc = Doc { text: String::from("Lifetimes: a contract"), title: "" };
doc.title = &doc.text[..9];
println!("{}", doc.title);
let moved = doc;
println!("{}", moved.text);
}
8 | doc.title = &doc.text[..9];
| -------- borrow of `doc.text` occurs here
9 | println!("{}", doc.title);
10 | let moved = doc;
| ^^^
| |
| move out of `doc` occurs here
| borrow later used here
The usual ways out are to store a position (title_end: usize) or to keep the owner and the view in separate values; Lesson 15 has arenas and indices. Two routes leave the safe subset, each with its obligation stated in its own lesson: a value that promises not to move (Pin, Lesson 18) and raw pointers behind an unsafe proof (Lessons 19 and 20).Every region so far was chosen by a caller or by a struct's owner. One is chosen by no one.
6 · 'static: a region, and a bound that only looks like one
'static is the lifetime whose region is the rest of the program. As a reference lifetime it says the referent is valid for the whole remaining run; string literals have it, because their bytes are embedded in the program (Lesson 02). It also appears in a second position, as a bound: T: 'static means that every lifetime parameter of T outlives 'static. An owned String has no lifetime parameters, so it satisfies the bound although it is dropped long before the program ends. In short, T: 'static says T holds no borrow of anything that ends. A function can ask for either, and the compiler checks them alike:
fn need_ref(_s: &'static str) {}
fn need_bound<T: 'static>(_t: T) {}
fn main() {
let s = String::from("x");
need_ref("literal");
need_bound(String::from("x"));
need_bound(vec![1, 2, 3]);
need_ref(&s);
need_bound(&s);
}
error[E0597]: `s` does not live long enough
--> src/main.rs:10:16
|
10 | need_bound(&s);
| -----------^^-
| | |
| | borrowed value does not live long enough
| argument requires that `s` is borrowed for `'static`
11 | }
| - `s` dropped here while still borrowed
|
note: requirement that the value outlives `'static` introduced here
--> src/main.rs:2:18
|
2 | fn need_bound<T: 'static>(_t: T) {}
| ^^^^^^^
The literal, the String and the Vec pass both. A reference to a local passes neither: need_ref(&s) and need_bound(&s) both fail with E0597, and the second message, quoted above, adds a note that points at the bound. You will meet the bound again on thread::spawn (Lesson 16): a spawned thread can outlive the function that spawned it, so what it captures must hold no borrow of anything that ends.
The temptation in front of a lifetime error is the word itself, and the compiler sometimes offers it:
fn get_str() -> &str {
"ada"
}
fn main() {
println!("{}", get_str());
}
error[E0106]: missing lifetime specifier
--> src/main.rs:1:17
|
1 | fn get_str() -> &str {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
1 | fn get_str() -> &'static str {
| +++++++
help: instead, you are more likely to want to return an owned value
|
1 - fn get_str() -> &str {
1 + fn get_str() -> String {
|
The first suggestion fits this body, which returns a literal, and the compiler says it is uncommon. A function that returns a borrow needs something to borrow from, and 'static claims there is such a thing forever. Adding it to silence an error promises the whole program what the function cannot keep. The remedy is usually the opposite: return an owned value (Lesson 03), as the second suggestion does, or find the input the result should borrow from and name it. That is the first step of the order that closes this part.
7 · When the checker says no
Lessons 03 to 08 built one checker in layers. The law (Lesson 05) says which overlaps are forbidden; regions and liveness (Lessons 06 and 07) say how long a borrow matters; signatures (this lesson) carry that across calls. Every rejection comes from one of those layers, which is what makes it possible to triage. The order matters, because each question is cheaper and safer than the next. It is the order proposed in Crichton's 2020 paper on the usability of ownership, with a clause about signatures added to the second step.
| # | ask | if yes | met in this part |
|---|---|---|---|
| 1 | Does the program really break the law: can you name the effect through one name and the later use of another? | The compiler is right. Change the program; do not silence the message. | §2: b is used after it ends |
| 2 | If not, can you restructure so the checker's local view suffices: end a borrow earlier, reorder, split a struct, pass an index, return an owned value, or change what the signature promises? | Restructure. Most rejections end here. | §5: -> &'a str; Lesson 07 |
| 3 | Is there a library function that already packages the proof? | Use it: split_at_mut, mem::take, the map's entry. | Lessons 04, 07 |
| 4 | Only then: must you take the proof back with unsafe? | Keep it small, state the obligation, wrap it (Lessons 19, 20). | §5 side box |
Run it on the first borrow error of this lesson, the E0597 of §2. Question 1: yes, under the signature: the result may be y, and y's owner is gone. This body would not misbehave at run time, but a caller may rely on the signature only. Question 2: three restructurings exist, each with a price. Keep b alive past the use; use r inside the block; or narrow the contract to x only (the second program of §2), which obliges the body to return something derived from x. Questions 3 and 4 are never reached. A .clone(), a 'static or an unsafe block would each silence the error by editing a layer other than the one that was wrong, which is why the order asks the cheaper, safer questions first.
Common mistakes / failure modes
'a says how long the value lives"x, the caller knows the result comes from x"T: 'static means the value lives for the whole program"T holds no borrow of anything that ends. A String satisfies it and is dropped long before the program does (§6)..clone() is the fix"Checkpoint exercise
[E0621 | ok]: the else { y } branch returns something the signature did not promise, and the call is fine because the result is tied to x. Tying the result to both inputs makes the body pass and turns the right half into E0597, since b dies first: [ok | E0597]. The failure moved from the callee to the caller and did not go away. Editing the body instead, so that both branches return x, passes both halves, but the function can no longer return the longer string.Where this points next
With signatures the last part of the checker is in place: a law, regions, liveness, and a contract that lets each function be checked alone. Its price is what this part has shown: a vocabulary of owners, borrows and regions, and an order in which to ask why it said no. The notation cannot yet say one thing. A function that searches for a word and finds none has nothing to return: a reference is always valid, so there is no null, and a moved-from name is dead, so there is no empty state to leave behind. How do we say "no value", or "one of several shapes", without inventing null? That is Lesson 09.
'static is a region; T: 'static is a bound, and means T holds no borrow of anything that ends. When the checker says no, ask in order: a real violation, a restructuring, a library helper, and only then unsafe.Interview prompts
- Why must a Rust function state, in its signature, which input its result borrows from? (§1 — a caller cannot read bodies without making checking non-modular, some callees have no body, and an inferred contract would change whenever the body did.)
- What does
fn f<'a>(x: &'a str, y: &str) -> &'a strpromise, and who checks what? (§2, §3 — the result borrows fromxonly; the body is checked to return nothing shorter than'a, each caller to keepxalive, neither reading the other, so a caller is judged by the signature and never the body.) - State the three elision rules and what they read. (§4 — each elided input lifetime is its own; one lifetime-bearing parameter gives its lifetime to the result;
&selfgives its lifetime to the result; the signature text only.) - A method
fn next(&mut self) -> &stron a struct holding&'a strstops callers holding two results. Why, and what is the fix? (§5 — rule 3 ties the result to the exclusive borrow ofself; write-> &'a strso it borrows from the text.) - What is the difference between
&'static strandT: 'static? (§6 — a reference valid for the whole program, against a bound sayingTholds no borrow of anything that ends; an ownedStringsatisfies the bound.) - Why is
&mut Tinvariant inT? (§2 side box — otherwise a short-lived reference could be stored through it into a place the caller believes holds a longer-lived one.) - The compiler says "lifetime may not live long enough". How do you decide what to change? (§3, §7 — the body and the signature disagree; decide from the callers which half states the intended contract before accepting the suggested edit.)
Companion reads: C++ · 04 The heap and the lifetime problem (dangling returns, unchecked), C++ · 19 Undefined behavior, and Go · 03 Pointers, heap and GC (the collector answers the same question at run time).