Guide contents

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

Rust✗ does not compile · E0597
fn main() {
    let reference;
    {
        let value = String::from("temporary");
        reference = &value;
    } // `value` is dropped here
    println!("{reference}");
}
Output
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 here

The 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.

Rust✗ does not compile · E0106
fn longer(a: &str, b: &str) -> &str {
    if a.len() >= b.len() { a } else { b }
}

fn main() {
    println!("{}", longer("apple", "fig"));
}
Output
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.

Rust
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));
}
Output
borrow checker

Read 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

Rust
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
    }
}
Output
inside the block: a long outer string

5. 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.

Rust✗ does not compile · E0597
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}");
}
Output
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 here

This 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.

Rust✗ does not compile · E0597
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}");
}
Output
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 here

The 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.

Rust
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`
}
Output
reports

Compare 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.

Rust✗ does not compile · E0621
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", "/"));
}
Output
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.

Rust✗ does not compile · E0515
fn make_greeting<'a>(name: &'a str) -> &'a str {
    let greeting = format!("Hello, {name}!");
    &greeting
}

fn main() {
    println!("{}", make_greeting("Ferris"));
}
Output
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 function

10. 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.

Rust
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
}
Output
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.

Rust
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));
}
Output
lifetime / lifetime

12. Lifetimes work the same for slices

Rust
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}");
}
Output
worst latency: 480

13. 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.

Rust✗ does not compile · E0502
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}");
}
Output
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 here

The 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.

Rust
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}");
}
Output
inside: Content-Type = text/html
still valid: Content-Type

15. A lifetime with a &mut parameter

A returned reference can come from a mutable borrow. The result then keeps the argument mutably borrowed.

Rust
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);
}
Output
added ADA
["ADA", "GRACE"]

16. Returning Option<&T>

References inside Option, Result, tuples, or Vec follow the same rules.

Rust
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"));
}
Output
Some("bella")
None

17. Returning a Vec of references

A result can hold many references into the input. None of the text is copied.

Rust
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);
}
Output
["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.

Rust
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"));
}
Output
a literal that is longer
also a literal

19. Returning a reference into a struct

Rust
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);
}
Output
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.

Rust
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));
}
Output
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.

Rust
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);
}
Output
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:

Rust✗ does not compile · E0716
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);
}
Output
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

Rust
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"),
    }
}
Output
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.

Rust
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));
    }
}
Output
LOG_LEVEL: Some("debug")
PORT: Some("8080")
TIMEOUT: None

The 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.

Rust
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));
}
Output
["let", "total", "=", "price", "+", "tax", ";"]

25. Realistic example: searching records without copying them

Rust
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);
    }
}
Output
K-2 Compact Keyboard 5900

Notice 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.

Rust
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);
}
Output
["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.

Rust
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));
}
Output
checked at compile time

Summary

  • 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 T means 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.

Next: Lifetimes in Structs & Methods.