A lifetime is the stretch of a program during which a reference is valid. Every reference in Rust has one. Most of the time the compiler works lifetimes out on its own and you never see them, as in every lesson so far. They become visible in two places: when a function returns a reference and the compiler cannot tell which input it came from, and when a struct stores a reference instead of owning its data. Then you write lifetime annotations such as 'a, which look strange at first but say something simple: "this reference lives at least as long as that one".
Lifetimes are the part of Rust that learners find hardest, mostly because they are explained as something you control. They are not. A lifetime annotation never makes a value live longer or shorter. It is a label that describes relationships the borrow checker then verifies. This lesson explains why lifetimes exist, how to annotate functions, methods and structs, the three elision rules that let you leave most annotations out, what 'static really means, the lifetime errors you will hit most often and how to read them, and when the best fix is to stop borrowing and own the data. Interviewers commonly ask you to explain fn longest<'a>(x: &'a str, y: &'a str) -> &'a str, the elision rules, and the difference between &'static T and T: 'static.
Why lifetimes exist
Lesson 5 showed the borrow checker refusing a dangling reference, a reference to memory that has already been freed. Here is the smallest example again. This does not compile:
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r = {r}");
}
error[E0597]: `x` does not live long enough
--> dangling_err.rs:5:13
|
4 | let x = 5;
| - binding `x` declared here
5 | r = &x;
| ^^ borrowed value does not live long enough
6 | }
| - `x` dropped here while still borrowed
7 | println!("r = {r}");
| - borrow later used here
Draw the scopes and the problem is obvious. x lives only inside the inner block, but r, which points at x, is used after the block ends:
let r; ---------------------+-- 'r (r is used until the println)
{ |
let x = 5; --+-- 'x |
r = &x; | |
} --+ x dropped |
println!("r = {r}"); ---------------------+ r still points at x: dangling
The borrow checker compares the two regions. The reference &x must be valid for the whole of 'r, wherever r is used, but the value x exists only for 'x, which is shorter. A reference may never outlive the value it points to, so the program is rejected. This is the entire idea of lifetimes: every reference must be valid for every place it is used.
In C, this program compiles and reads freed stack memory: undefined behaviour that might print 5, print garbage or crash. Java and Python avoid the problem with a garbage collector, which keeps x alive as long as anything refers to it. Rust has no garbage collector, so it proves at compile time that the situation cannot arise. Lifetimes are the vocabulary for that proof. They exist only during compilation; at run time a reference is a plain pointer, and lifetime checking costs nothing.
Inside a function body, the compiler infers every lifetime itself. Annotations are needed only at boundaries the compiler checks one function at a time: function signatures and type definitions.
Lifetimes in function signatures
Here is a function that returns the longer of two string slices. It does not compile:
fn longest(x: &str, y: &str) -> &str {
if x.len() >= y.len() { x } else { y }
}
fn main() {
println!("{}", longest("apple", "fig"));
}
error[E0106]: missing lifetime specifier
--> longest_err.rs:1:33
|
1 | fn longest(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`
help: consider introducing a named lifetime parameter
|
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
Reading the compiler error: E0106, missing lifetime specifier
The help line states the problem exactly: the return type contains a borrowed value, but the signature does not say whether it is borrowed from x or y. Why does the compiler care, given that the body plainly returns one of them?
Because Rust checks each function call using only the callee's signature, never its body. When main calls longest(&a, &b) and keeps the result, the compiler must know how long the result may be used. If the result borrowed from a, it is valid while a is; if from b, while b is. The signature must say. Checking against signatures keeps compilation fast and means changing a function's body can never break its callers.
The suggested fix introduces a lifetime parameter:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
fn main() {
let city = String::from("Thiruvananthapuram");
let result;
{
let other = String::from("Pune");
let inner = longest(&city, &other);
println!("inside the block: {inner}");
result = longest(&city, "Kochi");
}
println!("after the block: {result}");
}
Output:
inside the block: Thiruvananthapuram
after the block: Thiruvananthapuram
How to read fn longest<'a>(x: &'a str, y: &'a str) -> &'a str:
<'a>declares a lifetime parameter, just as<T>declares a type parameter. Lifetime names start with an apostrophe and are usually short:'a,'b, or a descriptive name like'input.x: &'a strandy: &'a strsay both arguments are valid for at least the region'a.-> &'a strsays the result is valid for that same region'a.
At each call, the compiler chooses 'a to be the region where both arguments are valid, in practice the shorter of the two. The result may then be used only within that region. The annotation does not change how long city or other live; it tells the compiler which constraint to check.
In the example, the first call borrows city and other, so 'a can be no longer than other's scope, and inner is used inside the block: fine. The second call borrows city and a string literal, both of which outlive the block, so result may be used after it.
Reading the compiler error: E0597, the result outlives a shorter argument
Move the use of the result outside the block where one argument lives, and the annotation does its job. This does not compile:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
fn main() {
let city = String::from("Thiruvananthapuram");
let result;
{
let other = String::from("Pune");
result = longest(&city, &other);
}
println!("longest: {result}");
}
error[E0597]: `other` does not live long enough
--> longest_scope_err.rs:10:33
|
9 | let other = String::from("Pune");
| ----- binding `other` declared here
10 | result = longest(&city, &other);
| ^^^^^^ borrowed value does not live long enough
11 | }
| - `other` dropped here while still borrowed
12 | println!("longest: {result}");
| ------ borrow later used here
As it happens, longest would return city here, which lives long enough. The compiler does not know that, because it does not look inside longest. The signature says the result may borrow from either argument, so it must assume the worst: the result might point into other, which is dropped at the closing brace while result is still used afterwards.
city ------------------------------------------+ (lives to end of main)
other ----------+ (dropped at }) |
'a ----------+ = where BOTH are valid |
result used X <- outside 'a: error
The fix depends on intent. Use the result inside the block, move other to the outer scope, or, if the result can only ever come from one argument, say so in the signature, as the next section shows.
Different lifetimes for different inputs
A lifetime parameter should connect the output only to the inputs it can really borrow from. This function always returns its first argument, so the second argument needs no relationship to the result:
fn first_of<'a>(x: &'a str, _y: &str) -> &'a str {
x
}
fn main() {
let kept = String::from("kept");
let result;
{
let temporary = String::from("temporary");
result = first_of(&kept, &temporary);
}
println!("{result}");
}
Output:
kept
_y: &str has its own, separate lifetime, which the compiler fills in automatically. The result is tied only to x, so temporary can be dropped while result is still in use. Had the signature used 'a for both, the program would fail exactly like longest_scope_err.rs. Precise signatures give callers more freedom.
Interview tip
When asked what 'a in longest means, avoid saying "it makes the references live for 'a". Say: "It is a constraint, not a command. It tells the compiler the returned reference is valid only as long as both inputs are, so callers cannot use the result after either input is dropped. The compiler picks the concrete region at each call site."
The lifetime elision rules
Lessons 5 to 12 contained many functions that returned references without any 'a, such as fn first_word(text: &str) -> &str. They compiled because of lifetime elision: a small set of rules the compiler applies to fill in lifetimes in signatures where the answer is unambiguous. If the rules leave any output lifetime undecided, you get E0106 and must write it yourself.
The rules apply to fn signatures (and impl headers):
- Each input reference gets its own lifetime parameter.
fn f(x: &str, y: &str)becomesfn f<'a, 'b>(x: &'a str, y: &'b str). - If there is exactly one input lifetime, it is assigned to every output reference.
fn f(x: &str) -> &strbecomesfn f<'a>(x: &'a str) -> &'a str. - If there are several input lifetimes but one of them is
&selfor&mut self, the lifetime ofselfis assigned to every output reference. This is why methods returning references to their own fields rarely need annotations.
written after elision
------- -------------
fn first_word(s: &str) -> &str fn first_word<'a>(s: &'a str) -> &'a str rule 2
fn longest(x: &str, y: &str) -> &str fn longest<'a,'b>(x: &'a str, y: &'b str)
-> &'? str ERROR: rules 2, 3 do not apply
fn title(&self) -> &str fn title<'a>(&'a self) -> &'a str rule 3
fn line(&self, n: usize) -> &str fn line<'a>(&'a self, n: usize) -> &'a str rule 3
longest fails because it has two input lifetimes and no self, so neither rule 2 nor rule 3 decides the output. Here are the functions that elision handles:
fn first_word(text: &str) -> &str {
text.split_whitespace().next().unwrap_or("")
}
struct Document {
title: String,
body: String,
}
impl Document {
fn title(&self) -> &str {
&self.title
}
fn line(&self, number: usize) -> &str {
self.body.lines().nth(number).unwrap_or("")
}
}
fn main() {
println!("[{}]", first_word(" hello brave new world"));
let doc = Document {
title: String::from("Notes"),
body: String::from("line zero\nline one\nline two"),
};
println!("{}: {}", doc.title(), doc.line(1));
}
Output:
[hello]
Notes: line one
Writing out fn first_word<'a>(text: &'a str) -> &'a str would mean exactly the same thing. Clippy's needless_lifetimes lint flags such explicit annotations and suggests removing them, so idiomatic code writes lifetimes only where elision cannot.
Reading the compiler error: elision picks self, but you returned something else
Rule 3 is a guess: "a method returning a reference probably returns part of self". When that guess is wrong, the error can be confusing. This does not compile:
struct Document {
title: String,
}
impl Document {
fn title_or<'a>(&self, fallback: &'a str) -> &str {
if self.title.is_empty() {
fallback
} else {
&self.title
}
}
}
fn main() {
let doc = Document {
title: String::new(),
};
println!("{}", doc.title_or("untitled"));
}
error: lifetime may not live long enough
--> method_err.rs:8:13
|
6 | fn title_or<'a>(&self, fallback: &'a str) -> &str {
| -- - let's call the lifetime of this reference `'1`
| |
| lifetime `'a` defined here
7 | if self.title.is_empty() {
8 | fallback
| ^^^^^^^^ method was supposed to return data with lifetime `'1` but it is returning data with lifetime `'a`
|
help: consider reusing a named lifetime parameter and update trait if needed
|
6 | fn title_or<'a>(&self, fallback: &'a str) -> &'a str {
| ++
The output &str was elided, so rule 3 tied it to &self, which the compiler calls '1. But one branch returns fallback, which has lifetime 'a, and nothing says 'a lasts as long as '1. "Lifetime may not live long enough" means the returned reference's region might end before the region the signature promised.
The help line's fix, -> &'a str, would only move the problem to the other branch, because &self.title is not tied to 'a. The method can return either one, so both inputs and the output need the same lifetime:
struct Document {
title: String,
}
impl Document {
fn title_or<'a>(&'a self, fallback: &'a str) -> &'a str {
if self.title.is_empty() {
fallback
} else {
&self.title
}
}
}
fn main() {
let blank = Document {
title: String::new(),
};
let named = Document {
title: String::from("Budget 2026"),
};
println!("{}", blank.title_or("untitled"));
println!("{}", named.title_or("untitled"));
}
Output:
untitled
Budget 2026
This is the longest pattern in method form: when the result can come from either of two places, tie both to the output.
Structs that hold references
A struct may store references, but then it needs a lifetime parameter, and an instance must not outlive the data it borrows. This lets you build lightweight views over existing data without copying it, as parsers and tokenizers commonly do.
#[derive(Debug)]
struct Highlight<'a> {
text: &'a str,
line: usize,
}
struct Tokens<'a> {
rest: &'a str,
}
impl<'a> Tokens<'a> {
fn new(input: &'a str) -> Self {
Tokens { rest: input }
}
fn next_token(&mut self) -> Option<&'a str> {
let trimmed = self.rest.trim_start();
if trimmed.is_empty() {
return None;
}
let end = trimmed.find(' ').unwrap_or(trimmed.len());
let (token, rest) = trimmed.split_at(end);
self.rest = rest;
Some(token)
}
}
fn find_highlights<'a>(text: &'a str, word: &str) -> Vec<Highlight<'a>> {
let mut found = Vec::new();
for (i, line) in text.lines().enumerate() {
if line.contains(word) {
found.push(Highlight {
text: line,
line: i + 1,
});
}
}
found
}
fn main() {
let poem = String::from("the rain in pune\nfalls mainly\non the plain rain-soaked roads");
let marks = find_highlights(&poem, "rain");
for h in &marks {
println!("line {}: {:?}", h.line, h.text);
}
println!("{marks:?}");
let mut tokens = Tokens::new(" let x = 42 ;");
let mut all = Vec::new();
while let Some(t) = tokens.next_token() {
all.push(t);
}
println!("{all:?}");
}
Output:
line 1: "the rain in pune"
line 3: "on the plain rain-soaked roads"
[Highlight { text: "the rain in pune", line: 1 }, Highlight { text: "on the plain rain-soaked roads", line: 3 }]
["let", "x", "=", "42", ";"]
Reading the new syntax:
struct Highlight<'a> { text: &'a str, ... }says aHighlightholds a reference that is valid for'a, so aHighlight<'a>cannot be used after that data is gone.find_highlights<'a>(text: &'a str, word: &str) -> Vec<Highlight<'a>>connects the results totextonly. The search word is not stored, so it does not constrain the results. EachHighlightpoints intopoem; no line is copied.impl<'a> Tokens<'a>declares the lifetime for theimplblock, exactly likeimpl<T>.next_token(&mut self) -> Option<&'a str>deliberately returns'a, not the elided lifetime of&mut self. The tokens point into the original input, not into theTokensstruct, so they remain valid after the borrow oftokensends. With the elided form,Option<&str>, every token would keeptokensmutably borrowed, and the loop that collects them intoallwould fail with E0499, "cannot borrowtokensas mutable more than once at a time".
poem: String (heap) "the rain in pune\nfalls mainly\non the plain..."
^ ^
| |
marks[0].text -------+ marks[1].text ------+ (views, no copies)
Reading the compiler error: E0597, a struct outliving its data
This does not compile:
struct Highlight<'a> {
text: &'a str,
}
fn main() {
let saved;
{
let note = String::from("remember the milk");
saved = Highlight { text: ¬e[9..] };
}
println!("{}", saved.text);
}
error[E0597]: `note` does not live long enough
--> struct_outlive_err.rs:9:36
|
8 | let note = String::from("remember the milk");
| ---- binding `note` declared here
9 | saved = Highlight { text: ¬e[9..] };
| ^^^^ borrowed value does not live long enough
10 | }
| - `note` dropped here while still borrowed
11 | println!("{}", saved.text);
| ---------- borrow later used here
It is the same E0597 as the very first example, because a struct holding a reference is subject to the same rule as the reference itself. saved.text points into note, which is dropped at the end of the block. The fixes are to keep note alive in the outer scope, or to make the struct own its text with a String field.
The anonymous lifetime '_
When a type has a lifetime parameter but you do not need to name it, write '_, the anonymous lifetime: fn parse(input: &str) -> Tokens<'_>. It means "apply the elision rules here", and it makes visible that the returned value borrows from an argument. Writing plain Tokens with no lifetime also compiles, but it hides the borrow, and Rust 1.99 warns about it by default:
warning: hiding a lifetime that's elided elsewhere is confusing
--> parse_hidden.rs:5:17
|
5 | fn parse(input: &str) -> Tokens {
| ^^^^ ^^^^^^ the same lifetime is hidden here
| |
| the lifetime is elided here
|
= note: `#[warn(mismatched_lifetime_syntaxes)]` on by default
help: use `'_` for type paths
|
5 | fn parse(input: &str) -> Tokens<'_> {
| ++++
The 'static lifetime
'static is the one lifetime with a reserved name. It means "valid for the entire run of the program". It appears in two forms that are easy to confuse.
&'static T is a reference that is valid forever. String literals are the common case: the text of "OK" is stored in the program's binary, so a &'static str pointing at it can never dangle. Values in static items are also 'static.
T: 'static is a bound on a type. It means "this type contains no references that could expire", in other words "values of this type can be kept for as long as you like". Every owned type with no borrowed fields satisfies it: String, Vec<i32>, u64, and also &'static str. It does not mean the value lives forever; an owned String passed to such a function is still dropped normally.
use std::fmt::Debug;
static APP_NAME: &str = "lesson-13";
fn status_text(code: u16) -> &'static str {
match code {
200 => "OK",
404 => "Not Found",
_ => "Unknown",
}
}
fn keep_forever<T: Debug + 'static>(value: T) -> String {
format!("{value:?}")
}
fn main() {
println!("{APP_NAME}: {} {}", status_text(200), status_text(404));
let owned = String::from("an owned String is 'static");
println!("{}", keep_forever(owned));
println!("{}", keep_forever(vec![1, 2, 3]));
println!("{}", keep_forever("a literal"));
}
Output:
lesson-13: OK Not Found
"an owned String is 'static"
[1, 2, 3]
"a literal"
status_textreturns&'static str. All its return values are literals, so the caller can keep them as long as it likes. Elision does not apply here because there are no input references, so'staticmust be written.static APP_NAME: &strneeds no'staticon the reference: instaticandconstitems, references are'staticautomatically.keep_foreverrequiresT: 'static. An ownedStringand aVecqualify because they borrow nothing; a string literal qualifies because its borrow is'static.
A borrow of a local variable does not qualify. This does not compile:
use std::fmt::Debug;
fn keep_forever<T: Debug + 'static>(value: T) -> String {
format!("{value:?}")
}
fn main() {
let local = String::from("local data");
println!("{}", keep_forever(&local));
}
error[E0597]: `local` does not live long enough
--> static_err.rs:9:33
|
8 | let local = String::from("local data");
| ----- binding `local` declared here
9 | println!("{}", keep_forever(&local));
| -------------^^^^^^-
| | |
| | borrowed value does not live long enough
| argument requires that `local` is borrowed for `'static`
10 | }
| - `local` dropped here while still borrowed
|
note: requirement that the value outlives `'static` introduced here
--> static_err.rs:3:28
|
3 | fn keep_forever<T: Debug + 'static>(value: T) -> String {
| ^^^^^^^
The note points at the bound that demanded 'static. You will meet this exact error with std::thread::spawn, which requires its closure to be 'static because the new thread may outlive the function that started it (lesson 16).
&'static T a reference valid for the whole program
e.g. "literal", &STATIC_ITEM
T: 'static a type holding no short-lived borrows
e.g. String, Vec<u8>, i32, &'static str
(the value itself can still be dropped at any time)
Pitfall: adding 'static to silence an error
When the compiler says a borrow does not live long enough, writing 'static on the signature almost never helps; it only demands something harder to satisfy, and the error moves to the caller. Fix the real cause: shorten the use, lengthen the owner, or own the data.
Returning references to local data
The most common lifetime question from beginners is "how do I return a reference to something I created in this function?". The answer is: you cannot, and you should not want to. This does not compile:
fn greeting(name: &str) -> &str {
let message = format!("Hello, {name}!");
&message
}
fn main() {
println!("{}", greeting("Asha"));
}
error[E0515]: cannot return reference to local variable `message`
--> return_local_err.rs:3:5
|
3 | &message
| ^^^^^^^^ returns a reference to data owned by the current function
Reading the compiler error: E0515
E0515 is the plainest lifetime error. message is a String owned by greeting, and it is dropped when the function returns, so any reference to it would dangle. Notice that the signature compiled: elision tied the result to name. The body is rejected because &message does not borrow from name. No annotation can fix this, because the data really does disappear. The fix is to return the owned value: fn greeting(name: &str) -> String and message without the &. Returning a String moves ownership to the caller, which costs no copy of the text, only of the three-word String header.
When to own the data instead
Lifetimes on structs spread. A Config<'a> forces every struct that contains a Config to have a lifetime too, and every function that returns one to tie it to some input. Borrowing struct fields pays off for short-lived views, such as a tokenizer over input that outlives it. For data that is built once and kept, owning is simpler.
Here is a configuration loader that owns its strings:
#[derive(Debug)]
struct Config {
name: String,
port: u16,
}
fn parse_config(text: &str) -> Option<Config> {
let mut name = None;
let mut port = None;
for line in text.lines() {
let (key, value) = line.split_once('=')?;
match key.trim() {
"name" => name = Some(value.trim().to_string()),
"port" => port = value.trim().parse().ok(),
_ => {}
}
}
Some(Config {
name: name?,
port: port?,
})
}
fn load() -> Option<Config> {
let file_contents = String::from("name = billing\nport = 8080");
parse_config(&file_contents)
}
fn main() {
match load() {
Some(cfg) => println!("{} listens on port {} ({cfg:?})", cfg.name, cfg.port),
None => println!("invalid config"),
}
}
Output:
billing listens on port 8080 (Config { name: "billing", port: 8080 })
load creates file_contents locally (standing in for reading a file) and returns a Config built from it. If Config held &str fields pointing into file_contents, load could not return it: the text is dropped at the end of load, and you would get E0515. Because Config owns Strings, the to_string() calls copy the two short values and the struct is free to outlive its source.
| Situation | Prefer |
|---|---|
| A function reads its input and returns part of it | Borrow: fn f(s: &str) -> &str |
| A short-lived view, parser or iterator over data that outlives it | A struct with a lifetime, Tokens<'a> |
| Data parsed or built once and kept around | Own it: String, Vec<T> |
| Data stored in a long-lived collection, sent to a thread or returned from a loader | Own it |
| Several owners need the same data without copying | Rc<T> or Arc<T> (lessons 15 and 16) |
Idiom: start owned, borrow when profiling says so
When designing a struct, start with owned fields. Introduce lifetimes when you have a clear reason, such as a hot loop that would otherwise allocate a String per token. A few clones on small strings cost far less than a program you cannot change.
A borrow that lasts as long as the result
One more pattern catches experienced programmers. When a method takes &mut self and returns a reference, rule 3 ties the result to the mutable borrow. As long as the result is in use, the struct stays mutably borrowed. This does not compile:
struct Inbox {
messages: Vec<String>,
}
impl Inbox {
fn newest(&mut self) -> &str {
self.messages.sort();
self.messages.last().map(|s| s.as_str()).unwrap_or("")
}
}
fn main() {
let mut inbox = Inbox {
messages: vec![String::from("b"), String::from("a")],
};
let top = inbox.newest();
println!("{} messages", inbox.messages.len());
println!("top: {top}");
}
error[E0502]: cannot borrow `inbox.messages` as immutable because it is also borrowed as mutable
--> mut_borrow_err.rs:17:29
|
16 | let top = inbox.newest();
| ----- mutable borrow occurs here
17 | println!("{} messages", inbox.messages.len());
| ^^^^^^^^^^^^^^ immutable borrow occurs here
18 | println!("top: {top}");
| --- mutable borrow later used here
newest needs &mut self to sort the messages, and its result borrows from that &mut self. While top is alive, inbox is exclusively borrowed, so reading inbox.messages.len() is refused, even though newest has finished running. The fixes are to finish using top before touching inbox again, to split the method into a mutating sort_messages(&mut self) and a reading newest(&self) -> &str, or to return an owned String. Separating "change" from "look" is usually the cleanest design.
Lifetimes in other places, briefly
A few more forms you will read in library code:
- Several lifetime parameters:
struct Pair<'a, 'b> { left: &'a str, right: &'b str }lets the two references have independent lifetimes. - Outlives bounds:
'long: 'shortmeans'longlasts at least as long as'short.T: 'ameans every reference insideTlives at least as long as'a. These are uncommon in application code. impl Traitand lifetimes: in the 2024 edition, a returnedimpl Traitautomatically captures every lifetime in scope, sofn words(s: &str) -> impl Iterator<Item = &str>just works. Theuse<..>syntax, stable since Rust 1.82, narrows what is captured when you need to.- Lifetimes in trait objects:
Box<dyn Trait>meansBox<dyn Trait + 'static>by default; a trait object that borrows needsBox<dyn Trait + 'a>.
Idioms and pitfalls
Idiom: let elision do the work
Write lifetimes only when the compiler asks for them. When it does, ask one question: "Which input can the result borrow from?" and tie the output to exactly those inputs.
Pitfall: thinking annotations extend lifetimes
'a cannot keep a value alive. If a value is dropped too early, the only fixes are to move it to a longer-lived owner, use the reference sooner, or own a copy.
Pitfall: one lifetime for everything
Tying every reference to a single 'a compiles more often at first but over-constrains callers, as in longest_scope_err.rs. Give unrelated inputs their own lifetimes, or leave them elided.
Pitfall: self-referential structs
A struct cannot safely hold both a String and a &str pointing into that same String: moving the struct would invalidate the reference. Store an index range (start..end) instead of the slice, or keep the owner and the view in separate values.
Exercises
Exercise 1: a word iterator that borrows
Write struct Words<'a> { rest: &'a str } and implement Iterator with Item = &'a str, yielding each run of letters and digits in the text. The yielded words must be slices of the original string, not new Strings.
Solution
The item type uses 'a, the lifetime of the text, not the lifetime of &mut self, so the words stay valid after iteration ends and can be collected.
struct Words<'a> {
rest: &'a str,
}
impl<'a> Iterator for Words<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
let trimmed = self.rest.trim_start_matches(|c: char| !c.is_alphanumeric());
if trimmed.is_empty() {
return None;
}
let end = trimmed
.find(|c: char| !c.is_alphanumeric())
.unwrap_or(trimmed.len());
let (word, rest) = trimmed.split_at(end);
self.rest = rest;
Some(word)
}
}
fn main() {
let text = String::from("Hello, world! Rust's borrow-checker, explained.");
let words: Vec<&str> = Words { rest: &text }.collect();
println!("{words:?}");
println!("{} words", words.len());
}
["Hello", "world", "Rust", "s", "borrow", "checker", "explained"]
7 words
The apostrophe and hyphen are not alphanumeric, so "Rust's" yields "Rust" and "s", and "borrow-checker" yields two words.
Exercise 2: longest word from a slice of slices
Write longest_word that takes &[&str] and returns the longest word, or None for an empty slice. The result must stay usable after the vector of words is dropped, as long as the original text is alive.
Solution
There are two lifetimes in &[&'a str]: the borrow of the slice (elided) and the lifetime 'a of the strings inside it. The result is tied only to 'a.
fn longest_word<'a>(words: &[&'a str]) -> Option<&'a str> {
let mut best: Option<&'a str> = None;
for &w in words {
if best.is_none_or(|b| w.len() > b.len()) {
best = Some(w);
}
}
best
}
fn main() {
let text = String::from("kerala karnataka goa assam");
let winner;
{
let words: Vec<&str> = text.split(' ').collect();
winner = longest_word(&words);
}
println!("{winner:?}");
println!("{:?}", longest_word(&[]));
}
Some("karnataka")
None
words is dropped at the end of the block, but winner borrows from text, which is still alive. With the signature fn longest_word<'a>(words: &'a [&'a str]) -> Option<&'a str>, the result would be tied to the vector too, and printing winner after the block would fail with E0597.
Exercise 3: split a header without annotations
Write split_pair(line: &str) -> Option<(&str, &str)> that splits "key: value" at the first colon and trims both parts. Explain why no lifetime annotation is needed.
Solution
There is exactly one input lifetime, so elision rule 2 assigns it to both references in the output.
fn split_pair(line: &str) -> Option<(&str, &str)> {
let (key, value) = line.split_once(':')?;
Some((key.trim(), value.trim()))
}
fn main() {
let header = String::from("Content-Type: text/html ");
if let Some((k, v)) = split_pair(&header) {
println!("key = [{k}], value = [{v}]");
}
println!("{:?}", split_pair("no colon here"));
}
key = [Content-Type], value = [text/html]
None
Exercise 4: fix the loader
This does not compile. Explain the error and fix it.
struct User<'a> {
name: &'a str,
city: &'a str,
}
fn parse_user(line: &str) -> Option<User<'_>> {
let (name, city) = line.split_once(',')?;
Some(User {
name: name.trim(),
city: city.trim(),
})
}
fn read_users() -> Vec<User<'static>> {
let file = String::from("Asha, Chennai\nVikram, Jaipur");
file.lines().filter_map(parse_user).collect()
}
fn main() {
for u in read_users() {
println!("{} lives in {}", u.name, u.city);
}
}
error[E0515]: cannot return value referencing local variable `file`
--> ex_owned_err.rs:16:5
|
16 | file.lines().filter_map(parse_user).collect()
| ----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `file` is borrowed here
Solution
Each User<'_> borrows from file, a String owned by read_users that is dropped when the function returns, so the users cannot leave the function (E0515). Writing 'static in the return type does not help, because the data is not 'static. The fix is to make User own its fields:
#[derive(Debug)]
struct User {
name: String,
city: String,
}
fn parse_user(line: &str) -> Option<User> {
let (name, city) = line.split_once(',')?;
Some(User {
name: name.trim().to_string(),
city: city.trim().to_string(),
})
}
fn read_users() -> Vec<User> {
let file = String::from("Asha, Chennai\nVikram, Jaipur\nbroken line");
file.lines().filter_map(parse_user).collect()
}
fn main() {
let users = read_users();
for u in &users {
println!("{} lives in {}", u.name, u.city);
}
println!("{users:?}");
}
Asha lives in Chennai
Vikram lives in Jaipur
[User { name: "Asha", city: "Chennai" }, User { name: "Vikram", city: "Jaipur" }]
filter_map(parse_user) also skips the malformed third line, because parse_user returns None when there is no comma.
Exercise 5: tie only what you return
Write pick(primary, backup, use_backup) that returns one of two strings, and label(value, unit) that always returns value. Show that the result of label can outlive unit.
Solution
pick can return either input, so both share 'a. label returns only value, so _unit gets its own elided lifetime.
fn pick<'a>(primary: &'a str, backup: &'a str, use_backup: bool) -> &'a str {
if use_backup { backup } else { primary }
}
fn label<'a>(value: &'a str, _unit: &str) -> &'a str {
value
}
fn main() {
let primary = String::from("db-primary");
{
let backup = String::from("db-backup");
println!("{}", pick(&primary, &backup, true));
}
let chosen = pick(&primary, "db-static-fallback", false);
println!("{chosen}");
let kept;
{
let unit = String::from("ms");
kept = label(&primary, &unit);
}
println!("{kept}");
}
db-backup
db-primary
db-primary
kept is printed after unit is dropped, which compiles only because label does not tie its result to unit.
Interview questions
Q1. What is a lifetime in Rust?
A lifetime is the region of code during which a reference is valid. The borrow checker ensures every reference's lifetime fits inside the lifetime of the value it points to, which rules out dangling references. Lifetimes exist only at compile time; they add no run-time cost or data.
Q2. Do lifetime annotations change how long values live?
No. Annotations describe relationships between the lifetimes of references, such as "the output lives no longer than both inputs", and the compiler checks the program against them. How long a value lives is decided by its owner's scope and moves, never by an annotation.
Q3. Explain fn longest<'a>(x: &'a str, y: &'a str) -> &'a str.
The function is generic over a lifetime 'a. Both inputs are valid for at least 'a, and the output is valid for 'a. At a call, the compiler picks 'a as the region where both arguments are valid, so the result cannot be used after either argument is dropped. Without the annotation the compiler cannot tell which input the result borrows from and reports E0106.
Q4. What are the lifetime elision rules?
First, each elided input reference gets its own lifetime. Second, if there is exactly one input lifetime, it is used for all elided output lifetimes. Third, if one of the inputs is &self or &mut self, its lifetime is used for all elided outputs. If the outputs are still undetermined after these rules, you must annotate.
Q5. Why does Rust need annotations in signatures if it can infer lifetimes in bodies?
The compiler checks each function separately and checks calls against signatures only, not bodies. The signature is therefore the contract that tells callers how long a returned reference may be used. This keeps checking local and fast, and guarantees that changing a function's body cannot silently break its callers.
Q6. What is the difference between &'static T and T: 'static?
&'static T is a reference valid for the whole program, such as a string literal. T: 'static is a bound meaning the type contains no non-'static references, so its values may be held indefinitely; owned types like String satisfy it even though each String is dropped normally. thread::spawn uses the bound so a thread cannot borrow a caller's local variables.
Q7. Why does a struct that holds a reference need a lifetime parameter?
The compiler must ensure no instance outlives the data its field points to. The parameter, as in struct Excerpt<'a> { part: &'a str }, ties each instance to the borrowed data, so using the struct after the data is dropped is a compile error. Owned fields need no lifetime.
Q8. How do you return a reference to a value created inside a function?
You cannot, because the value is dropped when the function returns and the reference would dangle; the compiler reports E0515. Return the owned value instead, such as String or Vec<T>. Moving it out is cheap because only the small header moves, not the heap data.
Q9. When should you avoid lifetimes in structs?
When the struct is long-lived, stored in collections, returned from loaders, sent across threads, or embedded in many other types. Lifetimes then spread through every containing type and function. Lifetimes in structs suit short-lived views over data that clearly outlives them, such as parsers, tokenizers and iterators.
Q10. Why can a method returning a reference keep the struct borrowed?
By elision rule 3, the returned reference gets the lifetime of the self borrow. If that borrow was &mut self, the struct stays exclusively borrowed for as long as the result is used, so even reads of the struct are rejected. Splitting the method into a mutating part and a &self reading part, or returning owned data, avoids it.
Q11. What is '_?
The anonymous lifetime. It asks the compiler to apply the elision rules, while making visible that a type borrows something, as in fn tokens(s: &str) -> Tokens<'_>. In impl headers it can stand for a lifetime you do not need to name, as in impl fmt::Display for Tokens<'_>.
Key takeaways
- A lifetime is the region where a reference is valid; references may never outlive what they point to.
- Lifetimes are a compile-time proof with no run-time cost; annotations describe relationships and never extend a value's life.
- Annotate when a function returns a reference and the compiler cannot tell which input it borrows from; tie the output only to the inputs it can come from.
- Three elision rules cover most signatures: one lifetime per input, a single input lifetime flows to outputs, and
&selfflows to outputs in methods. - A struct holding a reference needs a lifetime parameter and cannot outlive the borrowed data.
&'static Tlives for the whole program;T: 'staticmeans "owns its data or borrows only'staticdata".- You cannot return a reference to a local; return the owned value.
- Prefer owned fields in long-lived structs; use borrowed views for short-lived parsers and iterators.
Next lesson
Continue with Closures and iterators in Rust.

