Lesson 25 of 94
Lifetimes in Functions
Rust has no garbage collector, so the compiler must prove at compile time that every reference is used only while the value it points to is still alive. The span of code during which a reference is valid is its lifetime.
Most of the time the compiler works lifetimes out on its own, and you have been using references for several lessons without writing a single lifetime. This lesson is about the cases where it needs your help, and about what the annotations mean.
The single most important idea: a lifetime annotation never changes how long anything lives. It only describes a relationship between references ("the result points into this argument") so that the compiler can check callers against it.
1. Recap: a reference cannot outlive its value
fn main() {
let reference;
{
let value = String::from("temporary");
reference = &value;
} // `value` is dropped here
println!("{reference}");
}error[E0597]: `value` does not live long enough
--> src/main.rs:5:21
|
4 | let value = String::from("temporary");
| ----- binding `value` declared here
5 | reference = &value;
| ^^^^^^ borrowed value does not live long enough
6 | } // `value` is dropped here
| - `value` dropped here while still borrowed
7 | println!("{reference}");
| --------- borrow later used hereThe compiler compares two spans: how long value lives (until the inner }) and how long reference is used (until the println!). The use outlasts the value, so it rejects the code. Inside a single function, it can always see both spans.
2. Across a function call, the compiler needs a contract
This function returns one of its two arguments. Which one? It depends on the data at runtime, so the compiler cannot know from the body alone how long the result is valid.
fn longer(a: &str, b: &str) -> &str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
println!("{}", longer("apple", "fig"));
}error[E0106]: missing lifetime specifier
--> src/main.rs:1:32
|
1 | fn longer(a: &str, b: &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 `a` or `b`
help: consider introducing a named lifetime parameter
|
1 | fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
| ++++ ++ ++ ++Callers of longer only see its signature. The signature must say what the returned reference is tied to, so that each call can be checked without looking inside the function.
3. Adding a lifetime parameter
A lifetime parameter is written 'a (an apostrophe and a short name) and declared in angle brackets after the function name, like a generic type parameter.
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let first = String::from("borrow checker");
let second = String::from("lifetimes");
println!("{}", longer(&first, &second));
}borrow checkerRead the signature as: "for some lifetime 'a, both arguments live at least as long as 'a, and the result is valid for 'a". In practice, 'a becomes the shorter of the two input lifetimes, and the result can be used only while both arguments are still alive.
4. The result can be used while both inputs are alive
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let outer = String::from("a long outer string");
{
let inner = String::from("short");
let result = longer(&outer, &inner);
println!("inside the block: {result}"); // both still alive: fine
}
}inside the block: a long outer string5. Using the result after one input is gone is an error
Here result actually points to outer, which is still alive. But the signature says the result may point to either argument, so the compiler must assume the worst.
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let outer = String::from("a long outer string");
let result;
{
let inner = String::from("short");
result = longer(&outer, &inner);
}
println!("{result}");
}error[E0597]: `inner` does not live long enough
--> src/main.rs:10:33
|
9 | let inner = String::from("short");
| ----- binding `inner` declared here
10 | result = longer(&outer, &inner);
| ^^^^^^ borrowed value does not live long enough
11 | }
| - `inner` dropped here while still borrowed
12 | println!("{result}");
| ------ borrow later used hereThis is the key trade-off: the signature is a promise the compiler holds both sides to, and the check is done with the signature alone. That is what lets Rust check every function separately, and check millions of lines of code quickly.
6. Annotations don't extend lifetimes
Beginners sometimes add 'a hoping it will make a value "live longer". It can't. Annotations only connect references to each other; values still die at the end of their scope.
fn keep<'a>(s: &'a str) -> &'a str {
s
}
fn main() {
let kept;
{
let text = String::from("hello");
kept = keep(&text); // the annotation cannot keep `text` alive
}
println!("{kept}");
}error[E0597]: `text` does not live long enough
--> src/main.rs:9:21
|
8 | let text = String::from("hello");
| ---- binding `text` declared here
9 | kept = keep(&text); // the annotation cannot keep `text` alive
| ^^^^^ borrowed value does not live long enough
10 | }
| - `text` dropped here while still borrowed
11 | println!("{kept}");
| ---- borrow later used hereThe fix is always about ownership: make the value live longer (declare it in the outer scope), or return an owned value.
7. Tie the result only to what it really borrows from
If the result can only come from one argument, give only that argument the output's lifetime. The other argument gets no annotation and is then independent.
fn prefix_before<'a>(text: &'a str, separator: &str) -> &'a str {
match text.find(separator) {
Some(i) => &text[..i],
None => text,
}
}
fn main() {
let path = String::from("reports/2026/q3.csv");
let top_level;
{
let sep = String::from("/"); // short-lived
top_level = prefix_before(&path, &sep);
}
println!("{top_level}"); // fine: the result borrows only from `path`
}reportsCompare with example 5. A more precise signature gives callers more freedom.
8. Returning the wrong argument is caught inside the function
If the body returns something the signature did not promise, the error appears in the function, not at the call site.
fn prefix_before<'a>(text: &'a str, separator: &str) -> &'a str {
if text.is_empty() {
return separator; // not allowed: `separator` is not tied to 'a
}
text
}
fn main() {
println!("{}", prefix_before("a/b", "/"));
}error[E0621]: explicit lifetime required in the type of `separator`
--> src/main.rs:3:16
|
3 | return separator; // not allowed: `separator` is not tied to 'a
| ^^^^^^^^^ lifetime `'a` required
|
help: add explicit lifetime `'a` to the type of `separator`
|
1 | fn prefix_before<'a>(text: &'a str, separator: &'a str) -> &'a str {
| ++9. You can never return a reference to a local variable
No annotation can fix this: the local is dropped when the function returns.
fn make_greeting<'a>(name: &'a str) -> &'a str {
let greeting = format!("Hello, {name}!");
&greeting
}
fn main() {
println!("{}", make_greeting("Ferris"));
}error[E0515]: cannot return reference to local variable `greeting`
--> src/main.rs:3:5
|
3 | &greeting
| ^^^^^^^^^ returns a reference to data owned by the current function10. The fix: return an owned value
When a function creates data, return it owned. The caller becomes the owner and there is no lifetime to worry about.
fn make_greeting(name: &str) -> String {
format!("Hello, {name}!")
}
fn main() {
let g;
{
let name = String::from("Ferris");
g = make_greeting(&name);
}
println!("{g}"); // `g` owns its text, so `name` being gone doesn't matter
}Hello, Ferris!11. Many functions need no annotation at all
When a function has exactly one reference parameter, the compiler assumes the result borrows from it. These rules are called lifetime elision (Elision & 'static lists them). The two functions below are identical; the second just writes out what the compiler assumes.
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
fn first_word_explicit<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
fn main() {
let text = String::from("lifetime elision");
println!("{} / {}", first_word(&text), first_word_explicit(&text));
}lifetime / lifetime12. Lifetimes work the same for slices
fn largest<'a>(values: &'a [u32]) -> &'a u32 {
let mut best = &values[0];
for v in values {
if v > best {
best = v;
}
}
best
}
fn main() {
let latencies = vec![120, 480, 95];
let worst = largest(&latencies);
println!("worst latency: {worst}");
}worst latency: 48013. The result keeps its input borrowed
Because the returned reference borrows from the input, the input stays borrowed for as long as the result is used. You cannot change it in the meantime.
fn largest<'a>(values: &'a [u32]) -> &'a u32 {
let mut best = &values[0];
for v in values {
if v > best {
best = v;
}
}
best
}
fn main() {
let mut latencies = vec![120, 480, 95];
let worst = largest(&latencies);
latencies.push(1_000);
println!("worst latency: {worst}");
}error[E0502]: cannot borrow `latencies` as mutable because it is also borrowed as immutable
--> src/main.rs:14:5
|
13 | let worst = largest(&latencies);
| ---------- immutable borrow occurs here
14 | latencies.push(1_000);
| ^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
15 | println!("worst latency: {worst}");
| ----- immutable borrow later used hereThe signature -> &'a u32 is what tells the compiler that worst points into latencies. Copying the value out (let worst = *largest(&latencies);) ends the borrow immediately.
14. Two independent lifetime parameters
When a function returns references to two different inputs, it can use two lifetime parameters. Each part of the result is tied only to its own source.
fn split_pair<'a, 'b>(key_source: &'a str, value_source: &'b str) -> (&'a str, &'b str) {
let key = key_source.split(':').next().unwrap_or("");
let value = value_source.trim();
(key, value)
}
fn main() {
let header = String::from("Content-Type: ignored");
let key;
{
let raw_value = String::from(" text/html ");
let (k, v) = split_pair(&header, &raw_value);
println!("inside: {k} = {v}");
key = k; // `k` borrows only from `header`
}
println!("still valid: {key}");
}inside: Content-Type = text/html
still valid: Content-Type15. A lifetime with a &mut parameter
A returned reference can come from a mutable borrow. The result then keeps the argument mutably borrowed.
fn push_and_get<'a>(list: &'a mut Vec<String>, item: &str) -> &'a String {
list.push(item.to_uppercase());
&list[list.len() - 1]
}
fn main() {
let mut names = Vec::new();
let added = push_and_get(&mut names, "ada");
println!("added {added}");
push_and_get(&mut names, "grace");
println!("{:?}", names);
}added ADA
["ADA", "GRACE"]16. Returning Option<&T>
References inside Option, Result, tuples, or Vec follow the same rules.
fn find_by_prefix<'a>(names: &'a [String], prefix: &str) -> Option<&'a String> {
for n in names {
if n.starts_with(prefix) {
return Some(n);
}
}
None
}
fn main() {
let names = vec![String::from("alice"), String::from("bob"), String::from("bella")];
println!("{:?}", find_by_prefix(&names, "be"));
println!("{:?}", find_by_prefix(&names, "z"));
}Some("bella")
None17. Returning a Vec of references
A result can hold many references into the input. None of the text is copied.
fn long_words<'a>(text: &'a str, min_len: usize) -> Vec<&'a str> {
let mut words = Vec::new();
for w in text.split_whitespace() {
if w.len() >= min_len {
words.push(w);
}
}
words
}
fn main() {
let article = String::from("Lifetimes describe relationships between references, never durations");
let words = long_words(&article, 10);
println!("{:?}", words);
}["relationships", "references,"]18. String literals work with any lifetime
String literals live for the whole program (their lifetime is 'static), so they can be passed wherever a shorter lifetime is expected.
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let owned = String::from("owned text");
println!("{}", longer(&owned, "a literal that is longer"));
println!("{}", longer("tiny", "also a literal"));
}a literal that is longer
also a literal19. Returning a reference into a struct
struct User {
first_name: String,
last_name: String,
nickname: Option<String>,
}
fn display_name<'a>(user: &'a User) -> &'a str {
match &user.nickname {
Some(nick) => nick,
None => &user.first_name,
}
}
fn main() {
let a = User { first_name: String::from("Grace"), last_name: String::from("Hopper"), nickname: Some(String::from("Amazing Grace")) };
let b = User { first_name: String::from("Alan"), last_name: String::from("Turing"), nickname: None };
println!("{} ({})", display_name(&a), a.last_name);
println!("{} ({})", display_name(&b), b.last_name);
}Amazing Grace (Hopper)
Alan (Turing)20. Choosing from two sources with one lifetime
When the result may come from either input, both need the same lifetime. The call site then has to keep both alive while the result is used.
fn pick_greeting<'a>(personal: &'a str, default: &'a str, use_personal: bool) -> &'a str {
if use_personal && !personal.is_empty() { personal } else { default }
}
fn main() {
let default = String::from("Hello!");
let personal = String::from("Welcome back, Ada!");
for flag in [true, false] {
println!("{}", pick_greeting(&personal, &default, flag));
}
println!("{}", pick_greeting("", &default, true));
}Welcome back, Ada!
Hello!
Hello!21. The lifetime of a key vs the lifetime of the data
A lookup function's key is only needed during the call, so it should not share the result's lifetime. This lets callers pass a temporary key.
fn lookup<'a>(table: &'a [(String, String)], key: &str) -> Option<&'a str> {
for (k, v) in table {
if k == key {
return Some(v);
}
}
None
}
fn main() {
let table = vec![
(String::from("host"), String::from("db.internal")),
(String::from("port"), String::from("5432")),
];
let host = lookup(&table, &String::from("host")); // the key is a temporary
println!("{:?}", host);
}Some("db.internal")If key shared the lifetime 'a, the same call would fail. The temporary String is dropped at the end of the statement, but the signature would claim host might borrow from it:
fn lookup<'a>(table: &'a [(String, String)], key: &'a str) -> Option<&'a str> {
for (k, v) in table {
if k == key {
return Some(v);
}
}
None
}
fn main() {
let table = vec![(String::from("host"), String::from("db.internal"))];
let host = lookup(&table, &String::from("host"));
println!("{:?}", host);
}error[E0716]: temporary value dropped while borrowed
--> src/main.rs:12:32
|
12 | let host = lookup(&table, &String::from("host"));
| ^^^^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary value which is freed while still in use
13 | println!("{:?}", host);
| ---- borrow later used here
|
help: consider using a `let` binding to create a longer lived value
|
12 ~ let binding = String::from("host");
13 ~ let host = lookup(&table, &binding);
|22. Realistic example: the longest line of a document
fn longest_line(doc: &str) -> Option<(usize, &str)> {
let mut best: Option<(usize, &str)> = None;
for (i, line) in doc.lines().enumerate() {
let is_longer = match best {
Some((_, current)) => line.len() > current.len(),
None => true,
};
if is_longer {
best = Some((i + 1, line));
}
}
best
}
fn main() {
let doc = String::from("fn main() {\n let very_long_variable_name = compute_everything();\n}\n");
match longest_line(&doc) {
Some((n, line)) => println!("line {n} is longest ({} chars): {}", line.len(), line.trim()),
None => println!("empty document"),
}
}line 2 is longest (55 chars): let very_long_variable_name = compute_everything();23. Realistic example: layered configuration lookup
The primary and fallback tables share a lifetime because the result can come from either. The key is independent.
fn setting<'a>(primary: &'a [(&'a str, &'a str)], fallback: &'a [(&'a str, &'a str)], key: &str) -> Option<&'a str> {
let find = |table: &'a [(&'a str, &'a str)]| {
table.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
};
find(primary).or_else(|| find(fallback))
}
fn main() {
let env_vars = [("LOG_LEVEL", "debug")];
let defaults = [("LOG_LEVEL", "info"), ("PORT", "8080")];
for key in ["LOG_LEVEL", "PORT", "TIMEOUT"] {
println!("{key}: {:?}", setting(&env_vars, &defaults, key));
}
}LOG_LEVEL: Some("debug")
PORT: Some("8080")
TIMEOUT: NoneThe small closure find is defined inside the function to avoid repeating the search. Closures explains closures in detail.
24. Realistic example: a zero-copy tokenizer
Every token is a slice of the input, so tokenizing a large file allocates only the Vec of tokens, never the text itself.
fn tokenize(src: &str) -> Vec<&str> {
let mut tokens = Vec::new();
let mut start = None;
for (i, c) in src.char_indices() {
let is_word_char = c.is_alphanumeric() || c == '_';
match (start, is_word_char) {
(None, true) => start = Some(i),
(Some(s), false) => {
tokens.push(&src[s..i]);
start = None;
}
_ => {}
}
if "(){};=+".contains(c) {
tokens.push(&src[i..i + c.len_utf8()]);
}
}
if let Some(s) = start {
tokens.push(&src[s..]);
}
tokens
}
fn main() {
let source = String::from("let total = price + tax;");
println!("{:?}", tokenize(&source));
}["let", "total", "=", "price", "+", "tax", ";"]25. Realistic example: searching records without copying them
struct Product {
sku: String,
name: String,
price_cents: u32,
}
fn search<'a>(catalog: &'a [Product], query: &str, max_price: u32) -> Vec<&'a Product> {
let query = query.to_lowercase();
let mut hits = Vec::new();
for p in catalog {
if p.price_cents <= max_price && p.name.to_lowercase().contains(&query) {
hits.push(p);
}
}
hits
}
fn main() {
let catalog = vec![
Product { sku: String::from("K-1"), name: String::from("Mechanical Keyboard"), price_cents: 12_900 },
Product { sku: String::from("K-2"), name: String::from("Compact Keyboard"), price_cents: 5_900 },
Product { sku: String::from("M-1"), name: String::from("Wireless Mouse"), price_cents: 3_900 },
];
for p in search(&catalog, "keyboard", 10_000) {
println!("{} {} {}", p.sku, p.name, p.price_cents);
}
}K-2 Compact Keyboard 5900Notice query is shadowed by an owned lowercase copy inside the function. That's fine: it's never returned, so it doesn't affect the signature.
26. Realistic example: when to give up on references
References in return types are an optimization. If lifetimes become awkward, for example because the result must outlive all the inputs, return owned data. Here the report must outlive the raw log it was built from.
fn summarize(log: &str) -> Vec<String> {
let mut errors = Vec::new();
for line in log.lines() {
if let Some(message) = line.strip_prefix("ERROR ") {
errors.push(message.to_string()); // owned copy of just what we need
}
}
errors
}
fn main() {
let report;
{
let raw_log = String::from("INFO start\nERROR disk full\nINFO retry\nERROR disk still full");
report = summarize(&raw_log);
} // the raw log (possibly huge) is freed here
println!("{:?}", report);
}["disk full", "disk still full"]A good rule: borrow in parameters freely, and return references only when the result naturally lives inside an input and callers use it briefly.
27. Checked at compile time
Rust does not track at runtime which references are still alive, and values are freed at a precise moment. Instead, the work moves to compile time. Lifetime annotations are the small amount of information the compiler needs, at function boundaries, to do it.
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
fn main() {
let a = String::from("compile time");
let b = String::from("run time");
println!("checked at {}", longer(&a, &b));
}checked at compile timeSummary
- A lifetime is the span during which a reference is valid. The compiler checks that no reference outlives its value.
- Inside a function the compiler infers everything. At function boundaries, it sometimes needs annotations like
<'a>to know what a returned reference borrows from. fn f<'a>(x: &'a T, y: &'a T) -> &'a Tmeans the result is valid while both inputs are. Tie the result only to the parameters it really borrows from.- Annotations describe relationships. They never make anything live longer.
- You can never return a reference to a local. Return an owned value instead.
- A returned reference keeps its source borrowed until its last use.
