Lifetimes in Structs

Most structs you write hold owned data — a String, a Vec<T>, an i32 — and never need to think about lifetimes at all. But sometimes a struct only needs to borrow data that belongs to someone else, rather than pay the cost of copying or owning it. When a struct field is a reference, Rust needs a way to guarantee, at compile time, that the struct can never outlive the data it points into. That guarantee is expressed with a lifetime parameter on the struct itself. This lesson covers exactly how that works, why the compiler demands it, and how to use it correctly.

Overview: why structs need lifetime annotations

Every value in Rust already has an implicit lifetime — the scope during which it is valid, ending when it goes out of scope and is dropped. That is true whether or not you ever write the word "lifetime". The problem only appears when a struct stores a reference (&T) instead of an owned value. A reference is just a pointer with borrow-checker guarantees attached to it, and those guarantees have to be tracked somehow once the reference is tucked inside a struct.

Think of a struct holding a reference like a library card that lets you view a specific book without taking it home. The card is only useful as long as the book stays on the shelf. If the book is returned to another branch and destroyed (the original owner goes out of scope and the value is dropped), the card becomes worthless — and worse, in a language without Rust’s checks, code might still try to "read" that book through the stale card, producing a use-after-free bug. Rust refuses to let the card (the struct instance) exist for longer than the book (the borrowed data) is guaranteed to exist. It does this entirely at compile time, with zero runtime cost, by making you name the relationship explicitly: struct Excerpt<'a> { part: &'a str }. Here 'a is not a lifetime you invent or control directly — it is a generic parameter, exactly like <T>, except instead of standing in for a type, it stands in for "however long the borrowed data referenced by this field is guaranteed to remain valid." When you construct an Excerpt, the compiler substitutes the concrete scope of whatever you borrowed from, and then checks every use of that Excerpt value against that scope.

Elision does not apply to struct fields

You may already know that Rust often lets you omit lifetimes on functions and methods thanks to lifetime elision rules. That convenience does not extend to struct definitions. If a struct field is a reference, you must always write an explicit lifetime parameter on the struct — there is no elided form. Leaving it out is a compile error, not a style choice, and it is the single most common first mistake newcomers make (covered below in Common Mistakes).

Syntax

The general shape of a struct that borrows data looks like this:

struct StructName<'a> {
    field: &'a str,
}

impl<'a> StructName<'a> {
    fn new(field: &'a str) -> StructName<'a> {
        StructName { field }
    }
}
  • <'a> after the struct name declares a generic lifetime parameter, the same way <T> declares a generic type parameter. Lifetime names conventionally start with an apostrophe and a lowercase letter: 'a, 'b, and so on.
  • &'a str on a field means: this field holds a borrowed string slice, and the compiler guarantees that borrow stays valid for at least as long as 'a.
  • An impl block for a struct with a lifetime parameter must repeat it: impl<'a> StructName<'a>. The first <'a> introduces the parameter for the block; the second, on StructName<'a>, says which struct it applies to.
  • Inside methods, you can use 'a in a return type (for example -> &'a str) to tie the returned reference to the lifetime of the borrowed data the struct wraps, rather than to the shorter lifetime of the method’s own &self borrow. This distinction matters a lot and is demonstrated in Example 3 below.

Examples

Example 1: a simple wrapper around a borrowed string slice

struct Highlight<'a> {
    text: &'a str,
}

impl<'a> Highlight<'a> {
    fn new(text: &'a str) -> Highlight<'a> {
        Highlight { text }
    }

    fn shout(&self) -> String {
        format!("{}!!!", self.text.to_uppercase())
    }
}

fn main() {
    let sentence = String::from("rust is memory safe");
    let first_word = sentence.split_whitespace().next().unwrap();
    let highlight = Highlight::new(first_word);
    println!("{}", highlight.shout());
}

Output:

RUST!!!

Highlight<'a> stores a borrowed &'a str rather than an owned String, so building a Highlight costs nothing beyond copying a pointer and a length. first_word borrows from sentence, and the compiler infers 'a to be (at most) the scope during which sentence is alive. Because highlight is only used before sentence goes out of scope, the borrow checker is satisfied. Note that shout returns an owned String built with format!, not a reference — so its return type needs no lifetime at all.

Example 2: one lifetime parameter shared by multiple fields

struct Pair<'a> {
    first: &'a str,
    second: &'a str,
}

impl<'a> Pair<'a> {
    fn longer(&self) -> &'a str {
        if self.first.len() >= self.second.len() {
            self.first
        } else {
            self.second
        }
    }
}

fn main() {
    let city = String::from("Paris");
    let country = String::from("France");
    let pair = Pair { first: &city, second: &country };
    println!("Longer: {}", pair.longer());
}

Output:

Longer: France

Pair<'a> uses a single lifetime parameter for two fields borrowed from two different variables, city and country. That is allowed: 'a does not need to be the exact lifetime of either variable individually, only a lifetime that both borrows are guaranteed to outlive — the compiler picks the largest region that satisfies every constraint, which here is bounded by whichever of city or country is used last. Notice also that longer returns &'a str, explicitly tied to the struct’s own lifetime parameter, not to the lifetime of the &self borrow used to call the method — that is what lets the returned reference remain valid even after the call to longer() itself has returned.

Example 3: a stateful borrowing iterator

struct WordSplitter<'a> {
    remainder: &'a str,
}

impl<'a> WordSplitter<'a> {
    fn new(text: &'a str) -> WordSplitter<'a> {
        WordSplitter { remainder: text.trim() }
    }

    fn next_word(&mut self) -> Option<&'a str> {
        let remainder = self.remainder;
        if remainder.is_empty() {
            return None;
        }
        match remainder.find(' ') {
            Some(index) => {
                let word = &remainder[..index];
                self.remainder = remainder[index + 1..].trim_start();
                Some(word)
            }
            None => {
                self.remainder = "";
                Some(remainder)
            }
        }
    }
}

fn main() {
    let text = String::from("the quick brown fox");
    let mut splitter = WordSplitter::new(&text);

    while let Some(word) = splitter.next_word() {
        println!("word: {}", word);
    }
}

Output:

word: the
word: quick
word: brown
word: fox

This is the most realistic example: a hand-rolled word splitter that hands out slices of the original string without ever allocating a new String. The key trick is let remainder = self.remainder; at the top of next_word. Because &str implements Copy, this copies the reference value itself out of self into a local variable of type &'a str. From that point on, slices taken from remainder (like &remainder[..index]) carry the lifetime 'a baked into that reference, completely independent of the &mut self borrow used to call the method. That is exactly why the method can return Option<&'a str> — a reference that outlives the call to next_word itself — instead of being restricted to Option<&str> borrowed only for the duration of the call.

How it works step by step

When the compiler sees a struct definition with a reference field, it treats 'a as a real generic parameter of the type, the same way it treats T in Vec<T>. Concretely, here is what happens:

  • At definition time, struct Excerpt<'a> { part: &'a str } is parsed as a generic type over lifetimes; Excerpt is not a complete type on its own until 'a is filled in, exactly as Vec is not complete until T is filled in.
  • At construction time, when you write Excerpt { part: &some_string }, the compiler computes the region of code for which the borrow &some_string is valid, and unifies that region with 'a. The concrete type of the value becomes, effectively, Excerpt<'region-of-some_string>.
  • For every use of the resulting struct value (storing it in a variable, passing it to a function, returning it, calling a method on it), the borrow checker verifies that use happens strictly within the region bound to 'a. If you try to use the struct after the data it borrowed has been dropped, that use falls outside the region and the compiler rejects the program with a "does not live long enough" error — before the program ever runs.
  • Inside methods, elision rule three normally assigns any elided output lifetime to the lifetime of &self. But when you write the struct’s own 'a explicitly in the return type (as in fn longer(&self) -> &'a str), you override that default and tie the output to the lifetime of the borrowed data the struct wraps instead of the shorter borrow of the method call. Getting this distinction right is what allows iterator-style structs like WordSplitter to hand out references that survive past each individual method call.

Common Mistakes

Mistake 1: forgetting the lifetime parameter entirely

A struct field can never be a bare reference without a lifetime — there is no elision for struct fields.

struct Excerpt {
    part: &str,
}

This fails to compile with error[E0106]: missing lifetime specifier. The fix is to add the lifetime parameter to both the struct and the field:

struct Excerpt<'a> {
    part: &'a str,
}

Mistake 2: letting the struct outlive the data it borrows

struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let excerpt;
    {
        let text = String::from("temporary data");
        excerpt = Excerpt { part: &text };
    }
    println!("{}", excerpt.part);
}

Here text is dropped at the end of the inner block, but excerpt (which borrows from it) is used afterward in println!. The compiler rejects this with error[E0597]: `text` does not live long enough, catching a would-be dangling pointer before the program can run. The fix is to make sure the borrowed data outlives every use of the struct that borrows it — here, by declaring text in the outer scope:

struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let text = String::from("temporary data");
    let excerpt;
    {
        excerpt = Excerpt { part: &text };
    }
    println!("{}", excerpt.part);
}

Mistake 3: mutating the source while a struct still holds a borrow of it

struct Wrapper<'a> {
    text: &'a str,
}

fn main() {
    let mut message = String::from("hello world");
    let wrapper = Wrapper { text: &message };
    message.push_str("!");
    println!("{}", wrapper.text);
}

Because wrapper.text is used later in println!, the immutable borrow of message stored inside wrapper is still considered active at the point of message.push_str("!"), which needs a mutable borrow. Rust’s borrowing rule — one mutable borrow, or any number of immutable borrows, never both at once — applies just as strictly when the immutable borrow is stashed inside a struct field. This fails with error[E0502]: cannot borrow `message` as mutable because it is also borrowed as immutable. The fix is to finish using the borrow before mutating the source:

struct Wrapper<'a> {
    text: &'a str,
}

fn main() {
    let mut message = String::from("hello world");
    let wrapper = Wrapper { text: &message };
    println!("{}", wrapper.text);

    message.push_str("!");
    println!("{}", message);
}

Output:

hello world
hello world!

Thanks to non-lexical lifetimes (NLL), the compiler tracks the borrow only up to its last actual use (the first println!), so once that line has run, message is free to be borrowed mutably again.

Best Practices

  • Default to owned fields (String, Vec<T>) unless you have a concrete reason — usually performance, or the struct is short-lived and tied to data you already own elsewhere — to store a reference instead. Lifetime-annotated structs push complexity onto every caller.
  • Give lifetime parameters short, conventional names ('a, 'b) unless a longer name genuinely improves readability in a struct with several unrelated lifetimes.
  • When a struct has multiple reference fields, use one shared lifetime parameter unless the fields truly need independent lifetimes; only introduce a second parameter ('a, 'b) when the compiler actually forces you to by rejecting a shared one.
  • In methods, deliberately choose between tying a returned reference to 'a (the struct’s borrowed data) versus letting it default to the lifetime of &self — the former lets callers hold the result independently of the method call, as seen in the WordSplitter example.
  • Prefer building an iterator-style API (implementing the standard Iterator trait) over a bespoke next_word-style method when the struct is meant to be consumed in a loop — it plugs into for loops and iterator adapters like map and filter for free.
  • Read compiler errors carefully: messages like "does not live long enough" or "cannot borrow as mutable because it is also borrowed as immutable" point directly at the offending borrow and its conflicting use — they are almost always actionable as written.

Practice Exercises

  • Exercise 1: Write a struct Word<'a> that stores a single &'a str, plus a method is_palindrome(&self) -> bool that returns whether the word reads the same forwards and backwards (ignore case). Hint: you can build a reversed String with self.text.chars().rev().collect::<String>() and compare it to a lowercased version of the original.
  • Exercise 2: Implement the standard Iterator trait for a word-splitting struct like WordSplitter from Example 3, so it can be driven with a for word in splitter { ... } loop instead of a manual while let. Hint: impl<'a> Iterator for WordSplitter<'a> { type Item = &'a str; fn next(&mut self) -> Option<Self::Item> { ... } }.
  • Exercise 3: Write a struct KeyValue<'a> with fields key: &'a str and value: &'a str, and a function parse(input: &str) -> Option<KeyValue> that splits a string like "name=rust" on the first '=' character and returns Some(KeyValue { key: "name", value: "rust" }), or None if there is no '='. Hint: str::split_once('=') returns exactly the Option<(&str, &str)> pair you need.

Summary

  • A struct field that is a reference must always carry an explicit lifetime parameter — struct definitions never get lifetime elision the way function signatures do.
  • The lifetime parameter, written <'a>, is a generic parameter over "how long is this data guaranteed valid," and the compiler binds it to a concrete region whenever you construct the struct.
  • The borrow checker enforces, at compile time and with zero runtime cost, that a struct instance can never be used after the data any of its 'a-tagged fields point to has been dropped.
  • A single lifetime parameter can be shared across multiple fields borrowed from different variables; the compiler infers the largest region that satisfies every field.
  • Inside methods, writing the struct’s own 'a in a return type lets a returned reference outlive the method call itself, instead of being limited to the lifetime of the &self borrow — the technique behind borrowing iterators like WordSplitter.
  • The standard borrowing rule (one mutable borrow, or many immutable borrows, never both) applies just as strictly when the borrow is stored inside a struct field as when it is a plain local reference.
  • Default to owned fields unless you have a clear, deliberate reason to borrow — lifetime-annotated structs are a tool for specific situations, not a default style.