Advanced Lifetimes and Borrowing

Lifetime and Borrowing API Design

Designing ergonomic lifetime and borrowing APIs ensures callers get zero-copy performance when borrowing while never fighting unnecessary lifetime unification, invariant mutable lifetime traps, or hidden temporary borrows.

Rule 1: Keep Unrelated Lifetimes Separate When Returning Projections

Unifying two independent input lifetimes into the same 'a parameter (fn lookup<'a>(&'a self, key: &'a str) -> Option<&'a Value>) forces the caller's temporary key string to live just as long as the returned &'a Value reference!

Only tie the output lifetime to the input parameter from which the returned data is actually borrowed:

use std::collections::HashMap;

struct Registry {
    entries: HashMap<String, String>,
}

impl Registry {
    // GOOD: `key` has an independent anonymous lifetime; the returned `&str`
    // is tied only to `&self` (inferred automatically by lifetime elision):
    fn get(&self, key: &str) -> Option<&str> {
        self.entries.get(key).map(String::as_str)
    }
}

fn main() {
    let mut reg = Registry {
        entries: HashMap::new(),
    };
    reg.entries
        .insert(String::from("db_url"), String::from("postgres://localhost"));

    // Temporary key `format!(...)` is dropped at the end of the inner block,
    // while `found` stays alive for the lifetime of `&reg`!
    let found: &str = {
        let temp_key = format!("db_{}", "url");
        reg.get(&temp_key).unwrap()
    };

    println!("found: {}", found);
}
found: postgres://localhost

Rule 2: Never Write &'a mut Self<'a>

Writing &'a mut Parser<'a> (using the exact same lifetime 'a on both the outer mutable reference and the struct's covariant/invariant parameter) is a severe anti-pattern. Because &'a mut T is invariant in T, unifying the mutable borrow's lifetime with 'a borrows the struct mutably for its entire remaining lifetime 'a, making it impossible to call any second method on the struct ever again!

// ❌ does not compile
struct Cursor<'a> {
    bytes: &'a [u8],
}

impl<'a> Cursor<'a> {
    // Anti-pattern: `&'a mut self` locks `Cursor<'a>` for all of `'a`!
    fn advance_locked(&'a mut self) -> Option<u8> {
        let (&first, rest) = self.bytes.split_first()?;
        self.bytes = rest;
        Some(first)
    }
}

fn main() {
    let buf = [10, 20, 30];
    let mut cursor = Cursor { bytes: &buf };
    let _ = cursor.advance_locked();
    // Second call fails because the first call borrowed `cursor` for `'a`!
    let _ = cursor.advance_locked();
}
error[E0499]: cannot borrow `cursor` as mutable more than once at a time
  --> src/main.rs:20:13
   |
18 |     let _ = cursor.advance_locked();
   |             ------ first mutable borrow occurs here
19 |     // Second call fails because the first call borrowed `cursor` for `'a`!
20 |     let _ = cursor.advance_locked();
   |             ^^^^^^
   |             |
   |             second mutable borrow occurs here
   |             first borrow later used here

Leaving the &mut self lifetime elided (fn advance(&mut self) -> Option<&'a u8>) gives &mut self a short, fresh call-site lifetime 'short while still returning data tied to 'a:

struct Cursor<'a> {
    bytes: &'a [u8],
}

impl<'a> Cursor<'a> {
    // Idiomatic: `&mut self` uses a short anonymous borrow, while returning `&'a u8`:
    fn advance(&mut self) -> Option<&'a u8> {
        let (first, rest) = self.bytes.split_first()?;
        self.bytes = rest;
        Some(first)
    }
}

fn main() {
    let buf = [10, 20, 30];
    let mut cursor = Cursor { bytes: &buf };

    let a = cursor.advance();
    let b = cursor.advance();
    println!("a = {:?}, b = {:?}", a, b);
}
a = Some(10), b = Some(20)

Rule 3: Use the Anonymous Lifetime '_ in Return Types

When returning a struct or trait object that captures a borrow from an input parameter (fn iter(&self) -> Iter<'_, T>), always include '_ in the return type so readers immediately see that the returned value keeps self borrowed.

Summary of API Borrowing Idioms

Scenario Idiomatic Parameter / Return Signature
Read-only string or slice input Accept &str / &[T] (or impl AsRef<str> / impl AsRef<[u8]>) rather than &String / &Vec<T>
Returning a view borrowing self fn as_view(&self) -> View<'_> (explicit '_ signals ongoing borrow of self)
Advancing a parser over 'src fn next_token(&mut self) -> Option<Token<'src>> (&mut self lifetime kept distinct from 'src)
Accepting a closure borrowing local state F: FnMut(&Item) (relies on implicit for<'a> HRTB)