How do lifetimes work in struct definitions

Lifetimes in struct definitions ensure that references stored within the struct remain valid for the required duration.

The bookmark that outlives the book

You are writing a text parser. You extract a substring and stash it in a struct called Token. Later, you try to print the token, but the original string buffer got dropped. In Python, this works because everything is a reference and the garbage collector keeps things alive. In Rust, the compiler rejects the code. The struct holds a reference to memory that no longer exists. The fix is not magic. It is telling the compiler exactly how long that reference must stay valid. That is what lifetimes in struct definitions do.

What a lifetime parameter actually is

Think of a struct with a reference like a bookmark inside a book. The bookmark points to a specific page. If you shred the book, the bookmark is useless. It points to nothing. Rust refuses to let you keep the bookmark if the book might get shredded before you are done with it.

A lifetime parameter is a contract. It says, "This struct is only valid as long as the data it points to exists." You attach a label, like 'a, to both the struct and the reference. The label ties their lives together. The struct cannot outlive the data. If the data goes away, the struct becomes invalid, and the compiler prevents you from using it.

This contract is purely compile-time. The compiler does not insert any runtime checks. It does not track references at execution time. It simply refuses to generate machine code if the contract could be broken. You get safety without paying a performance tax.

Treat the lifetime label as a promise you make to the compiler. Keep the promise, and the code compiles. Break it, and you rewrite the scope.

The smallest possible struct with a lifetime

Here is the syntax. You add a lifetime parameter to the struct definition and apply it to every reference field.

struct ImportantExcerpt<'a> {
    // The 'a label ties the struct's validity to the reference.
    // The struct is only safe as long as the str lives.
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael.");
    // Create the struct. The lifetime 'a is inferred to be
    // as long as `novel` lives.
    let excerpt = ImportantExcerpt {
        part: &novel[..5],
    };

    println!("Excerpt: {}", excerpt.part);
    // `novel` is dropped here. `excerpt` is dropped here.
    // Order doesn't matter because both are valid until the end.
}

The struct ImportantExcerpt has a lifetime parameter 'a. The field part is a reference with that same lifetime. When you construct excerpt, the compiler looks at &novel[..5]. It sees that novel lives for the duration of main. It assigns that duration to 'a. The excerpt struct is now tagged with that lifetime. You can use excerpt anywhere novel is still alive.

The compiler infers the lifetime automatically. You do not write ImportantExcerpt::<'static> or similar. The type system matches the reference's scope to the struct's parameter. This inference keeps your code clean while preserving the safety guarantee.

Let the compiler bind the lifetime for you. You only write the label to establish the relationship.

How the compiler tracks these scopes

Lifetimes are a compile-time feature. They generate zero overhead at runtime. The compiler uses them to check that references are valid. When you define a struct with a lifetime parameter, you are creating a type that depends on a duration. The compiler tracks these durations as scopes.

When you pass a reference to the struct, the compiler calculates the scope of that reference. It binds the lifetime parameter to that scope. The struct inherits the constraint. Any code that tries to use the struct after the scope ends gets rejected. The compiler proves that the reference inside the struct cannot dangle.

This check happens at every usage site. If you pass the struct to a function, the function must accept a struct with a compatible lifetime. If you return the struct, the caller must ensure the data lives long enough. The lifetime parameter propagates through your code, enforcing safety everywhere.

Consider a function that takes your struct. The function signature must declare the same lifetime parameter. The compiler matches the incoming struct's lifetime to the function's parameter. If the function tries to store the struct in a global variable, the compiler rejects it. A global variable has a 'static lifetime. A struct tied to a local string cannot satisfy that requirement. The mismatch is caught before the binary is built.

Memory layout reinforces this model. A &str is just two words: a pointer to the first byte and a length. The struct containing it is also just two words. There is no hidden bookkeeping. The lifetime exists only in the type system. The machine code treats it exactly like a raw pointer and length. The safety comes from the rules you follow, not from runtime guards.

Trust the type system. It tracks scopes more reliably than human memory ever could.

A realistic view over a document

Structs with lifetimes shine when you are building views over larger data. You want to slice up a document without copying the text. Copying strings is expensive. References are cheap. They are just a pointer and a length.

struct Document<'a> {
    /// Borrowed title from the source text.
    title: &'a str,
    /// Borrowed body from the source text.
    body: &'a str,
}

fn main() {
    let source = String::from("# Title\n\nBody text here.");
    let parts: Vec<&str> = source.lines().collect();

    // `parts` contains slices of `source`.
    // The lifetime of these slices is tied to `source`.
    let doc = Document {
        title: parts[0].trim_start_matches('#'),
        body: parts[1],
    };

    // `doc` is valid as long as `source` is valid.
    println!("Doc: {} - {}", doc.title, doc.body);

    // If we dropped `source` here, `doc` would be invalid.
    // The compiler prevents using `doc` after `source` drops.
}

The Document struct holds references to the title and body. It does not own the text. It borrows from source. This avoids allocation. The struct is just two pointers. If you have a million documents, this saves megabytes of memory and allocation overhead. The trade-off is that doc cannot outlive source. You must manage the scope of source carefully.

Community convention uses 'a as the default name for the first lifetime. It is short, standard, and readable. Only use descriptive names like 'input if you have multiple lifetimes that need disambiguation. Most structs only need one lifetime. Stick with 'a.

You will often see this pattern in parsers, template engines, and UI frameworks. The heavy data stays in one place. Lightweight structs point into it. The compiler guarantees those pointers never wander into freed memory.

Keep your data in one place and let structs point to it. The memory savings compound quickly.

Where beginners trip up

Structs with lifetimes trip up beginners in two main ways. The first is forgetting the lifetime parameter. The second is trying to return a struct that references local data.

If you write struct Excerpt { part: &str }, the compiler rejects it with E0106 (missing lifetime specifier). Rust requires explicit lifetimes on structs because it cannot guess how long the reference must live relative to the struct itself. The compiler needs the parameter to enforce the contract. Add the lifetime and the error disappears.

The second error is E0597 (borrowed value does not live long enough). This happens when you try to return a struct from a function, but the data it points to is local to the function.

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

fn bad_excerpt() -> Excerpt {
    let text = String::from("Local data");
    // `text` is dropped at the end of this function.
    // Returning `Excerpt` would create a dangling reference.
    Excerpt { part: &text }
}

The compiler sees that text is dropped when bad_excerpt returns. The Excerpt would hold a reference to dead memory. The error points to the return statement. The fix is to either return an owned type like String or ensure the data lives longer than the function. The compiler is not being difficult. It is saving you from a crash that would happen three hours later in production. Trust the error.

Another common mistake is assuming lifetime elision applies to structs. Elision rules exist for functions and methods. They do not apply to struct definitions. The compiler cannot infer a struct's lifetime from its fields alone. You must write the parameter explicitly. This explicitness is a feature. It forces you to acknowledge the borrowing relationship upfront.

Read the error message carefully. It tells you exactly which scope ends too early. Extend the scope or change the ownership model.

Choosing between borrowed views and owned data

You do not always need lifetimes. Sometimes you should own the data. The choice depends on your use case.

If the struct needs to manage the data independently, use owned types. String owns its buffer. Vec<T> owns its elements. Structs with owned fields can be moved, returned, and stored without lifetime constraints. They are more flexible but more expensive. They allocate memory and copy data.

If you are processing data owned elsewhere, use lifetimes. Parsers, caches, and UI views often borrow data. They extract information without taking ownership. This is fast and memory-efficient. The struct is a window into the data. The window cannot exist without the wall.

Structs with lifetimes are also useful for grouping references that must live together. If you have multiple references that all point into the same buffer, a struct with a single lifetime ties them together. The compiler ensures they all stay valid as a group.

Use a struct with lifetime parameters when you are viewing or processing data owned elsewhere and want zero-copy performance. Use a struct with owned types like String or Vec<T> when the struct needs to own the data and manage its lifetime independently. Use Rc<T> or Arc<T> inside a struct when multiple owners are needed and the data must outlive the struct's immediate scope. Reach for lifetime parameters in structs when building parsers, caches, or views over larger datasets.

Lifetimes are the compiler's way of guaranteeing your pointers never dangle. Write them down, and the compiler does the heavy lifting.

Where to go next