TL;DR
Rust lifetimes explained — learn lifetime annotations, elision rules, and how the borrow checker validates reference validity
Key concepts
- Rust lifetimes explained
- Rust lifetime annotations
- Rust lifetime elision
- Rust reference lifetimes
Lifetimes
Lifetimes are Rust's way of ensuring that references are always valid. Every reference in Rust has a lifetime - the scope for which that reference is valid. Most of the time, lifetimes are inferred, but sometimes you need to annotate them explicitly.
What You'll Learn
By the end of this lesson you will fix a settings reader that does not compile: it hands out slices of a configuration string, and the caller wants to keep one of those slices after the reader itself is gone. The compiler refuses, the message points at the caller, and the fix is a single character in a signature three lines away. Getting from the message to that character is the skill; everything else here is in service of it.
You arrive here already holding the rule this lesson enforces — a reference must never outlive the value it borrows (Borrowing in Depth) — and able to see why a returned reference is the hard case. What is new is that inside one function body the compiler can check that rule by itself, while across a function signature it cannot, so the signature has to carry the answer. This is what lets the capstone's taskwork answer a query without copying anything: its select is declared fn select<'a>(tasks: &'a [Task], wanted: Option<Priority>) -> Vec<&'a Task>, returning a list of borrowed views into the caller's task list rather than clones of it. Worth knowing that the annotation there is not strictly forced — elision would have inferred the same relationship, because there is only one input lifetime it could come from — so the capstone writes it out to make the borrow visible at a glance. That is the second reason to be able to read these: not only to fix a signature that will not compile, but to see at once, from the signature alone, whether a function is going to hand you a copy or a view.
What Are Lifetimes?
A lifetime is the scope during which a reference is valid. Consider this:
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
// println!("{}", r); // ERROR: x doesn't live long enough
} // ---------+
The reference r has lifetime 'a, but it refers to x which only has lifetime 'b. Since 'b is shorter than 'a, using r after the inner block ends won't compile — that is why the println! is commented out. Uncomment it and run the snippet to see the borrow checker reject it with "x does not live long enough"; as written, the program compiles and prints nothing.
Lifetime Annotation Syntax
Lifetime annotations describe relationships between lifetimes:
&i32 // a reference
&'a i32 // a reference with explicit lifetime 'a
&'a mut i32 // a mutable reference with explicit lifetime 'a
When You Need Lifetime Annotations
The compiler needs help when:
- A function returns a reference
- A struct holds references
- Multiple references have ambiguous relationships
Function Signatures
// This won't compile - Rust doesn't know which input's lifetime to use
// fn longest(x: &str, y: &str) -> &str {
// if x.len() > y.len() { x } else { y }
// }
// Solution: annotate with lifetime 'a
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let string1 = String::from("long string");
let string2 = String::from("short");
let result = longest(&string1, &string2);
println!("The longest string is: {}", result);
}
The 'a annotation means: "the returned reference will be valid for the smaller of the two input lifetimes."
Lifetime Elision Rules
Rust has rules for inferring lifetimes so you don't always need annotations:
Rule 1: Each Input Gets Its Own Lifetime
// Written:
fn compare(a: &str, b: &str) { ... }
// Compiler infers a DISTINCT lifetime for each input reference:
fn compare<'a, 'b>(a: &'a str, b: &'b str) { ... }
This rule only ever assigns lifetimes to the inputs; the output, if there is one, is still elided at this point. Rules 2 and 3 are what decide it.
Rule 2: One Input Lifetime = Output Lifetime
// Written:
fn first_word(s: &str) -> &str { ... }
// Compiler infers:
fn first_word<'a>(s: &'a str) -> &'a str { ... }
Rule 3: &self Lifetime = Output Lifetime
impl MyStruct {
// Written:
fn get_name(&self) -> &str { ... }
// Compiler infers:
fn get_name<'a>(&'a self) -> &'a str { ... }
}
Those three rules are worth turning from syntax into a decision you can actually make. The program below declares three signatures, none of them carrying a single lifetime annotation. One of them — B — is commented out because the compiler refuses it; the other two build fine. Knowing which one fails is the easy half. Before you run it, work out the half that matters: run each signature through the three rules in order and say precisely which rule rescues A, which rescues C, and why every rule runs out on B.
Predict
Three signatures, zero lifetime annotations between them, and B is the one the compiler rejects. Work out the reasoning behind that split: which elision rule lets A compile untouched, which one lets C compile untouched, and which rule B falls short of.
// A: one input reference, one output reference.
fn head(s: &str) -> &str {
s.split(' ').next().unwrap_or("")
}
// B: two input references, one output reference. Commented out because
// exactly one of these three signatures is rejected, and leaving it in
// would stop the whole file from building.
// fn pick(a: &str, b: &str) -> &str {
// if a.len() >= b.len() { a } else { b }
// }
// C: &self plus another argument, one output reference.
struct Doc { body: String }
impl Doc {
fn tail(&self, from: usize) -> &str {
&self.body[from..]
}
}
fn main() {
println!("{}", head("hello world"));
// println!("{}", pick("aa", "b"));
println!("{}", Doc { body: String::from("abcdef") }.tail(3));
}As written the program prints hello and def, and B is the signature that had to be commented out. Uncomment pick and its call and the file stops compiling with error[E0106]: missing lifetime specifier, along with the blunt explanation that the signature "does not say whether it is borrowed from a or b". head is fine because rule 2 applies — one input reference, so the output must borrow from it. tail is fine because rule 3 applies — it is a method, so the output borrows from &self, which is correct since the slice comes out of self.body. Note that from: usize is not a reference and plays no part at all. Annotate pick as fn pick<'a>(a: &'a str, b: &'a str) -> &'a str, uncomment both lines, and the whole file compiles and prints hello, aa, def. The question elision answers is never "how much syntax can I leave out" — it is "can the compiler identify one source for the returned reference". When it can, you write nothing. When it cannot, you have to say.
Structs with References
When a struct holds references, you must annotate lifetimes:
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("Excerpt: {}", excerpt.part);
}
The annotation 'a means: "an instance of ImportantExcerpt can't outlive the reference it holds."
Methods with Lifetimes
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
// Lifetime elision: &self lifetime is used for return
fn level(&self) -> i32 {
3
}
// Return type uses 'a from struct
fn announce_and_return_part(&self, announcement: &str) -> &'a str {
println!("Attention please: {}", announcement);
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let excerpt = ImportantExcerpt {
part: novel.split('.').next().unwrap(),
};
println!("Level: {}", excerpt.level());
println!("Part: {}", excerpt.announce_and_return_part("Here it comes!"));
}
Look closely at why announce_and_return_part writes -> &'a str rather than letting elision handle it. Rule 3 would have given the output &self's lifetime, which would tie the returned slice to the struct value — but the data actually lives in the string the struct borrowed, which outlives the struct. Getting that distinction wrong is the single most common real lifetime bug, and it is invisible until a caller tries to keep the slice a moment longer than the struct. The program below has exactly that mistake. It looks completely ordinary; commit a hypothesis about which reference is really being tracked before you change anything:
Debug
This settings reader is supposed to hand out a slice of raw that stays valid after the temporary Settings value is gone. It does not compile: rustc says 'settings does not live long enough'. Nothing in main is wrong. Work out which lifetime the return type is actually promising, then fix the signature.
// A settings reader that hands out slices of the text it was built from.
struct Settings<'a> {
text: &'a str,
}
impl<'a> Settings<'a> {
fn new(text: &'a str) -> Settings<'a> {
Settings { text }
}
// Return the value for key, borrowed from the original text.
fn get(&self, key: &str) -> Option<&str> {
for line in self.text.lines() {
let (k, v) = line.split_once('=')?;
if k.trim() == key {
return Some(v.trim());
}
}
None
}
}
fn main() {
let raw = String::from("host = localhost
port = 8080");
// The Settings value is temporary scaffolding; the slice it produced is not.
let port;
{
let settings = Settings::new(&raw);
port = settings.get("port").expect("port should be present");
}
assert_eq!(port, "8080", "expected the port slice to outlive the Settings");
println!("port = {}", port);
}Expected output: port = 8080
The signature was under-promising. Elision rule 3 tied the returned &str to &self — the borrow of the Settings value — while the slice actually points into self.text, the original string, which outlives that value. So main is right to complain: it wants port after settings is gone, and the signature said that was not allowed. The fix is a single annotation, -> Option<&'a str>, which says the result borrows from the text rather than from the struct. Nothing about the runtime changes; a lifetime annotation only describes a relationship that already holds, it never extends anyone's life. This is also why key stays unannotated: the answer is never borrowed from the string you searched with.
Reading E0597 When It Blames the Wrong Line
That block is the best example on the track of a diagnostic that is completely accurate and points nowhere near the fix, so it is worth reading in full rather than skimming for a suggestion. This is what the lane prints:
error[E0597]: `settings` does not live long enough
--> /tmp/main.rs:30:16
|
29 | let settings = Settings::new(&raw);
| -------- binding `settings` declared here
30 | port = settings.get("port").expect("port should be present");
| ^^^^^^^^ borrowed value does not live long enough
31 | }
| - `settings` dropped here while still borrowed
32 |
33 | assert_eq!(port, "8080", "expected the port slice to outlive the Settings");
| --------------------------------------------------------------------------- borrow later used here
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0597`.
Read the spans by role, not top to bottom. A borrow-check error carries four, and each answers a different question. error[E0597] is a stable, searchable identifier — rustc --explain E0597 prints its general write-up, and the code alone already tells you which of the borrow checker's situations you are in: a value did not live long enough, as opposed to a conflicting borrow (E0502) or a use after move (E0382). The --> says where the offending borrow is taken. declared here says what was borrowed. dropped here while still borrowed says when it died. And the last span, borrow later used here, is the load-bearing one — it is the reason the other three are a problem at all. Test that claim rather than taking it: delete the assert_eq! and the error comes straight back with the println! named as the later use instead, and only when every subsequent read of port is gone does the program compile. So the conflict is between two locations, not a property of either one, and the last span is telling you which second location is currently making the first one illegal.
Now the part the message will not do for you. All four spans are in main, and main is correct — there is nothing to fix at any of the four. What is wrong is the promise made by get's signature up at line 12 — eighteen lines above the nearest span, and completely unmentioned by the error. The compiler cannot point there, because as far as it is concerned that signature is a fact it was told, not a suspect. A borrow-check error names the place where a promise was broken, never the place where the promise was made — so when every span looks reasonable, stop reading the error and go read the signature of whatever produced the reference.
Notice also that there is no help: here, and be glad. rustc's suggestions are excellent, and on a nearby class of error it will offer .clone() — which compiles, and which would have "solved" this by copying the configuration string to dodge a lifetime it could have simply stated. When a suggestion appears, the question is not whether it works but what it costs; here, the absence of one is honest, because the fix is a design statement rather than a mechanical edit.
Assembling a Borrowed View
The lesson's whole shape — make an owner, take a view of it, use the view — is five statements, and their order is not free. Here is the type those statements operate on, so you are not ordering lines against a struct you have to infer. It is the same shape as LogView below: one field holding a borrowed &'a str, and two methods that hand pieces of that borrow back out with the same 'a, not with the lifetime of &self.
struct Report<'a> {
text: &'a str,
}
impl<'a> Report<'a> {
fn new(text: &'a str) -> Report<'a> {
Report { text }
}
fn first_line(&self) -> &'a str {
self.text.lines().next().unwrap_or("")
}
fn line_count(&self) -> usize {
self.text.lines().count()
}
}
Arrange the code
These five lines build a String, append a second line to it, wrap it in a Report that borrows it, pull the first line back out and print it with the total count. The pieces are shuffled. Put them in the order that runs and prints 'header line (1 of 2)' — then answer the question the order is really asking: one of these pairs is held apart by something stronger than a missing name, because Report holds a borrow of raw for as long as it is alive. Which pair is it, and what does the compiler call the violation?
let report = Report::new(&raw);raw.push_str("\nsecond line");let mut raw = String::from("header line");let headline = report.first_line();println!("{} (1 of {})", headline, report.line_count());
The Static Lifetime
'static means the reference lives for the entire program duration:
fn main() {
// String literals have 'static lifetime
let s: &'static str = "I live forever!";
println!("{}", s);
}
Use 'static sparingly - it's usually a sign you should reconsider your design.
Before the patterns section, pin down what all of this is ultimately in service of. Lifetimes are not a fourth thing on top of ownership and borrowing — they are the machinery that makes one specific earlier rule checkable across a function boundary. Close the page and answer from memory:
Recall
Without scrolling up: Borrowing in Depth gave you a rule about a reference and the value it points to — the one that makes it impossible to hold a reference to something that has been freed. Which statement correctly connects that rule to what lifetime annotations are for?
Lifetimes are the same no-dangling-references rule from Borrowing in Depth, carried across a function boundary. Inside one body the borrow checker can see every scope and compare them itself. A signature, though, is opaque — the caller sees only the signature, never the body — so when a reference comes out, the signature must say which reference going in it is tied to. That is why annotations cluster on exactly two shapes: functions that return references and structs that hold them. And it is why an annotation never changes what the program does at runtime: like a type, it states a relationship the compiler then checks.
The Same Program Without the Question
The settings reader you just fixed is an ordinary shape — build a helper, take a view out of it, keep the view after the helper is gone. It is worth seeing what happens to that exact shape in a garbage-collected language, because the contrast says what the annotation actually buys:
Transfer
The two programs below are the same idea: build a temporary helper over some text, pull a piece of that text out through it, and use the piece after the helper has gone out of scope. The Rust version needed a lifetime annotation to compile; the JavaScript version was run as written and printed 'port = 8080' with no annotation of any kind. Which statement explains that difference correctly?
// The Rust half, complete and runnable, with the annotation in place.
// The JavaScript it is compared with is the same shape and needs nothing:
// let port;
// { const settings = makeSettings(text); port = settings.get("port"); }
// console.log("port =", port); // prints 8080
struct Settings<'a> {
text: &'a str,
}
impl<'a> Settings<'a> {
fn new(text: &'a str) -> Settings<'a> {
Settings { text }
}
// The 'a is the whole subject: the result borrows from the TEXT, not from self.
fn get(&self, key: &str) -> Option<&'a str> {
for line in self.text.lines() {
let (k, v) = line.split_once('=')?;
if k.trim() == key {
return Some(v.trim());
}
}
None
}
}
fn main() {
let raw = String::from("host = localhost
port = 8080");
let port;
{
let settings = Settings::new(&raw);
port = settings.get("port").expect("port should be present");
}
println!("port = {}", port);
}Multiple Lifetime Parameters
Sometimes you need multiple lifetime parameters:
fn longest_with_announcement<'a, 'b>(
x: &'a str,
y: &'a str,
ann: &'b str,
) -> &'a str {
println!("Announcement: {}", ann);
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("hello");
let s2 = String::from("world!");
let ann = String::from("Comparing strings");
let result = longest_with_announcement(&s1, &s2, &ann);
println!("Longest: {}", result);
}
Lifetime Bounds
You can specify that a generic type must live at least as long as a lifetime:
fn print_ref<'a, T>(t: &'a T)
where
T: std::fmt::Display + 'a,
{
println!("{}", t);
}
fn main() {
let x = 5;
print_ref(&x);
}
Common Lifetime Patterns
Three shapes cover almost every annotation you will write. They are shown in decreasing order of hand-holding: the first is fully annotated, the second has one note withheld, and the third has none — because by then the question is one you can answer.
Pattern 1: Input/Output Relationship
// <'a> on the signature, and the SAME 'a on the parameter and the return.
// That pair is the whole claim: the slice handed back points into s, so it
// is valid exactly as long as s is, and no longer.
fn first_word<'a>(s: &'a str) -> &'a str {
// Both arms return a piece of s — one a subslice, one s itself — which
// is what makes the claim in the signature true rather than merely
// stated. A body returning a local would not compile against it.
match s.find(' ') {
Some(pos) => &s[..pos],
None => s,
}
}
fn main() {
let sentence = String::from("hello world");
let word = first_word(&sentence);
println!("First word: {}", word);
}
This is the shape you could have written with no annotation at all: elision would have given both the parameter and the return the same lifetime automatically, because there is only one input reference to take it from. It is written out here so you can see what elision was doing on your behalf.
Pattern 2: Struct Holding a Reference
// A struct holding a reference must declare a lifetime parameter, because
// the struct cannot be allowed to outlive the data its field points at.
struct Parser<'a> {
input: &'a str,
position: usize, // owned, so it needs no lifetime of its own
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Parser<'a> {
Parser { input, position: 0 }
}
// One note is withheld here. Say why the return type is &'a str rather
// than plain &str, before you read the paragraph below.
fn remaining(&self) -> &'a str {
&self.input[self.position..]
}
}
fn main() {
let text = String::from("hello world");
let parser = Parser::new(&text);
println!("Remaining: {}", parser.remaining());
}
The withheld note: remaining returns &'a str rather than &str because the slice comes out of self.input, which points into text, not out of self. Elision would have tied the return to &self — the shorter of the two — and the result would stop being usable the moment the Parser was dropped, even though the text it points at is still perfectly alive. That is the same failure the debug block earlier in this lesson is built on, and it is why this one line is the only place in the struct where the annotation cannot be left off.
Pattern 3: Returning References from Methods
No notes on this one. Before reading on, answer one question about it: it returns references and yet carries no 'a anywhere — not on the struct, not on either method. Why is that correct here when Pattern 2 needed one?
struct Container {
data: Vec<String>,
}
impl Container {
fn get(&self, index: usize) -> Option<&String> {
self.data.get(index)
}
fn first(&self) -> Option<&String> {
self.data.first()
}
}
fn main() {
let container = Container {
data: vec![String::from("a"), String::from("b")],
};
if let Some(first) = container.first() {
println!("First: {}", first);
}
}
The answer is that Container owns its data. Vec<String> is not a reference — the container holds the strings themselves — so there is no borrowed field to outlive and nothing for a struct-level 'a to describe. The references the two methods return borrow from self, and tying them to self is exactly right, which is precisely what elision does for a method with a &self receiver. That is the rule worth carrying out of all three patterns: a lifetime parameter appears when a value holds or returns a reference to something it does not own, and nowhere else. Owning your data is the way to not need one.
Practice Exercise
Reading lifetime annotations is not the same as choosing one. This is a build task: a small program that reports its own pass/fail. You are given a LogView — a struct that borrows a block of log text and hands out slices of it without copying a single byte — and three stubbed methods. Run it as-is and it fails immediately, naming the first check that did not pass. Implement each until every check passes and it prints All checks passed.
The interesting part is already done for you and is worth reading before you write anything: every return type is annotated &'a str, the struct's lifetime, not &self's. That is the decision the debug block above was about, and the checks are built to depend on it — main deliberately lets the LogView die at a closing brace while the slices it produced live on. Elided signatures would tie those slices to the view and none of it would compile. Your job is only the three bodies.
Build
Finish the build. Three methods are stubbed out and the checks below them fail until each returns the right slice. Run it as-is to see which check fails first, decide what that method is missing, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.
// A log scanner that hands out slices of the text it was built from.
// It never copies a single byte: every &str it returns points into text.
//
// Every return type below is already annotated 'a — the STRUCT's lifetime, not
// &self's. That is the one decision this exercise turns on: elision would have
// tied these to &self, and the checks below would then refuse to compile.
struct LogView<'a> {
text: &'a str,
}
impl<'a> LogView<'a> {
fn new(text: &'a str) -> LogView<'a> {
LogView { text }
}
// TODO 1: return the FIRST line of self.text, or "" if there are none.
// self.text.lines() yields each line as a &str borrowed from the text.
fn first_line(&self) -> &'a str {
let _ = self.text;
""
}
// TODO 2: return the first line that CONTAINS needle, or None.
// Note that needle is deliberately NOT 'a — the answer is borrowed
// from self.text, never from the string you searched with.
fn find_line(&self, needle: &str) -> Option<&'a str> {
let _ = needle;
None
}
// TODO 3: return the LONGEST line in self.text, or "" if there are none.
fn longest(&self) -> &'a str {
""
}
}
// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
let log = String::from("INFO boot ok
ERROR disk full and getting fuller
WARN slow");
// Every borrowed slice must outlive the LogView it came from.
let first;
let found;
let longest;
{
let view = LogView::new(&log);
first = view.first_line();
found = view.find_line("ERROR");
longest = view.longest();
} // view is gone here — the slices are not.
assert_eq!(first, "INFO boot ok", "first_line should return the first line");
assert_eq!(
found,
Some("ERROR disk full and getting fuller"),
"find_line should return the first line containing the needle"
);
assert_eq!(
LogView::new(&log).find_line("nothing here"),
None,
"find_line should return None when no line matches"
);
assert_eq!(
longest, "ERROR disk full and getting fuller",
"longest should return the longest line"
);
println!("All checks passed.");
println!("first: {}", first);
println!("found: {}", found.unwrap());
println!("longest: {}", longest);
}Expected output: All checks passed.
first: INFO boot ok
found: ERROR disk full and getting fuller
longest: ERROR disk full and getting fuller
Once it passes, try two variations and predict each before running:
- Delete one
'a. Changefn longest(&self) -> &'a strtofn longest(&self) -> &strand predict which line the compiler will point at before running. Your body is still perfectly correct, but the program stops compiling:error[E0597]: view does not live long enough, pointing at the call and then at the closing brace whereviewis dropped "while still borrowed". Elision has now tied the returned slice to theLogViewthat dies there. Nothing about the data changed; only the promise in the signature did. - Annotate the wrong thing. In
find_line, change the signature tofn find_line<'n>(&self, needle: &'n str) -> Option<&'n str>and decide what the compiler will say before running. It rejects the body, not the call: the lines you are returning come fromself.text, and nothing lets the compiler believe they live as long asneedle. A lifetime annotation is a claim about where a reference came from, so pointing it at the wrong source is caught immediately.
Key Takeaways
- Lifetimes ensure references are always valid
- Most lifetimes are inferred by the compiler
- Use
'asyntax when the compiler needs help - Lifetime annotations describe relationships, they don't change how long things live
- Structs holding references need lifetime parameters
'staticmeans "lives for the entire program"- Lifetime elision rules reduce annotation boilerplate
Lifetimes are one of Rust's most powerful features for memory safety!
Next Steps
With lifetimes understood, you're ready for traits and generics — Rust's tools for writing flexible, reusable code that works across different types.
Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.