Why `struct Excerpt { part: &str }` Is Rejected on Sight
Function signatures can lean on elision; struct definitions cannot. Try to declare a struct that stores a borrow and you are stopped immediately:
struct Excerpt { part: &str }
// error[E0106]: missing lifetime specifier
// |
// | struct Excerpt { part: &str }
// | ^ expected named lifetime parameter
The reason is structural, not cosmetic. A function is executed per call, so its signature's lifetimes describe one bounded interaction: arguments arrive, work happens, a result leaves. A struct has no such boundary — an Excerpt value can be moved into a vector, returned from a function, stored in a field of another struct, and used days of program-time later. If part: &str were allowed, the type would say nothing about whose borrow it holds, so nothing could check that the data behind part is still alive at each of those later uses. The compiler refuses to define a type whose validity cannot be decided.
The fix names the relationship on the type itself:
struct Excerpt<'a> { part: &'a str }
fn first_sentence(text: &str) -> Excerpt<'_> {
let end = text.find('.').unwrap_or(text.len());
Excerpt { part: &text[..end] }
}
let novel = String::from("Call me Ishmael. Some years ago...");
let sentence = first_sentence(&novel);
println!("{sentence}");
Two things happened at once. The definition gained a parameter <'a> that every borrow inside it must use, and every use of the type must supply or elide that parameter — hence Excerpt<'_> in the return type above. Read the type out loud: "an Excerpt holding a borrow that lives for 'a". The owner of the data — here novel — must outlive every use of the excerpt, and the borrow checker tracks that the same way it tracked function returns in Lifetime Annotations.
Note what does not need a parameter: String, u64, Vec<u8>, and any other owned type own their data, so they carry no borrowed span to declare. Only reference fields — directly, or inside fields like Cow<'a, str> — force the parameter. A struct with one borrowed field takes one parameter; borrowing from two independent sources takes two: struct Cross<'a, 'b> { from_a: &'a str, from_b: &'b str }.
Parameters sit in the same generic list as type parameters, and order follows declaration:
struct Labeled<'a, T> { tag: &'a str, value: T }
let price = Labeled { tag: "total", value: 99.5_f64 };
println!("{} = {}", price.tag, price.value);
Here T absorbs the owned part and 'a covers only the borrowed tag — the struct is generic over both what it holds and how long the tag lasts. Uses of the type supply arguments in order, Labeled<'static, u64>, or let inference fill them in when the value's shape makes it unambiguous.
The <'_> in Excerpt<'_> earlier was elision doing its job on a function signature — but note the boundary carefully: the definition always names 'a explicitly, and only uses in signatures may elide. Any value-level annotation such as let e: Excerpt = ... (no angle brackets at all) is an error, not an omission.
Interviewers also like to ask what #[derive(Clone)] does with the parameter. The answer: the derived Clone copies the reference, not the data, and the clone keeps the same 'a — cloning an Excerpt<'a> yields another Excerpt<'a> borrowing the same source. Same for Copy, Debug, PartialEq: derived impls are generic over 'a automatically, so the parameter threads through them without any work from you.
Constructing, Passing, and Storing a Borrowing Struct
Once declared, Excerpt<'a> behaves like any other type with one extra rule: wherever the struct goes, the borrow goes with it. Construction ties the parameter to the argument you passed:
let owned = String::from("Ship's log, day 40");
let excerpt = Excerpt { part: &owned };
let literal = "fixed text";
let excerpt2 = Excerpt { part: literal }; // &'static str satisfies any 'a
The second line is the everyday case: a 'static borrow coerces to whatever 'a the context needs, exactly as it did for function arguments. Nothing special happens at construction — the special part is everything you might try afterwards.
Passing it on is free, because the parameter just follows the value. fn summarize(e: &Excerpt) -> &str { e.part } compiles with no annotations at all: one input lifetime (&Excerpt<'_>) and elision tie the output to it.
Storing it in another struct propagates the parameter. struct Report<'a> { title: String, body: Excerpt<'a> } is correct, and Report is now itself a borrowing type — anything holding a Report must keep novel alive. This propagation is why borrowed types tend to infect APIs: a single &'a str field turns a leaf struct into one that needs lifetime plumbing at every layer above it. Programmers who have fought this in real codebases usually respond by making the leaf own its data, which is the last chapter of this topic.
Collections work the same way, and they make the outlives obligation concrete:
let novel = String::from("Call me Ishmael.\nSome years ago...");
let mut summaries: Vec<Excerpt<'_>> = Vec::new();
for line in novel.lines() {
summaries.push(Excerpt { part: line });
}
// while `summaries` is alive: `novel` may not be dropped, moved, or mutated
Every excerpt in that vector points into novel's buffer, so the checker treats novel as borrowed for as long as any element is reachable — not just until the loop ends. Mutation counts too: pushing a String::push_str onto novel would need &mut, which the live excerpts veto.
And when the relationship is broken on purpose, the error names the exact value:
let dangling;
{
let local = String::from("scoped");
dangling = Excerpt { part: &local };
} // error[E0597]: `local` does not live long enough
// note: consider using a `let` binding to extend its lifetime
The struct type made this failure static: without the parameter, that code would compile and dangle; with it, the borrow checker sees that dangling outlives its source and refuses at compile time. The suggested fix — hoist the owner above the excerpt — is almost always the right first move, and the alternative is the owned-field design at the end of this topic.
Mutating the borrow is where designs get interesting. You may repoint the field:
fn repoint<'b>(e: &mut Excerpt<'b>, new_source: &'b str) {
e.part = new_source;
}
but the struct's parameter is fixed for the struct's lifetime, so the new borrow must match the declared 'b — you cannot swap in a shorter-lived source than the type promises. Mixing owned and borrowed fields works naturally, because only the borrowed ones need parameters:
struct Article<'a> { author: String, headline: &'a str, words: u32 }
Here words and author are declared once and cost nothing; headline drags a lifetime along. A common interview exercise is exactly this: "design a type that stores an author and a title, where the title may be borrowed from a parsed buffer" — the expected answer names one parameter, marks only the title field with it, and explains that dropping the borrow means dropping the parameter.
When to Delete the Parameter: `String`, `Cow`, or Borrow?
The choice of field type is the design decision, and the lifetime parameter is the price tag. Compare three honest designs for the same struct:
struct TitleA<'a> { text: &'a str } // no allocation, lifetime infects every API above
struct TitleB { text: String } // owns its data, one allocation on build
struct TitleC<'a> { text: Cow<'a, str> } // borrows when it can, allocates when it must
TitleB deletes the parameter, and that is the whole argument for it: a type without borrowed fields composes freely — it can be stored in globals, sent across threads (given Send fields), returned from functions with no <'_>, and kept as long as you like. The cost is the allocation and copy when you build it from a slice, paid once, at the boundary.
TitleC is the standard library's middle path. Cow<'a, str> is an enum — Borrowed(&'a str) or Owned(String) — whose methods let you work with both uniformly through Deref<Target = str>:
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<'_, str> {
if input.bytes().any(|b| b == b' ') {
Cow::Owned(input.replace(' ', "_")) // had to change it: pay
} else {
Cow::Borrowed(input) // nothing to do: free
}
}
let clean = normalize("health check");
println!("{clean}"); // works either way through Deref
Callers keep the lifetime parameter in their signatures but usually pay no allocation — the worst of both worlds only appears if they try to store the Cow past its source's life, which the type system prevents. Two conversion methods complete the tool: cow.into_owned() forces the owned variant (the escape hatch when you must store it), and cow.to_mut() gives you &mut String to edit in place, allocating on first use if you still held a borrow. The same pattern generalizes beyond strings: Cow<'a, [u8]> for protocol payloads, Cow<'a, Path> in file utilities — whenever "usually borrowed, sometimes rebuilt" describes your data.
A rough cost model helps decide. Passing a &str is passing a pointer and a length: a handful of nanoseconds, no matter how large the text. Building a String from a slice copies every byte and allocates, which is measured in tens of nanoseconds for small payloads and scales linearly with size — cheap per call, but not free inside a parser loop over millions of records. Borrowing, meanwhile, has a structural cost that no benchmark shows: every type downstream inherits the lifetime. The engineering question is never "is an allocation slow" — it is "how many times do I pay it, and how far does the borrow propagate if I avoid it".
Practical rules of thumb. Use a borrow when the struct is short-lived and allocation-sensitive (parsers, protocol decoders, per-request views). Use owned String when the value escapes its source — logs, caches, messages, anything crossing a thread or an await point. Reach for Cow when you are writing a library function that transforms strings and cannot know whether the transform allocates. And the conversion method exists for every escape hatch: excerpt.part.to_owned() produces a String and severs the borrow, letting you return data from a function whose source is a local — the trick you will reuse whenever a borrow error appears while building structs in later topics.
Quiz
1. Why must `struct Excerpt { part: &str }` gain a lifetime parameter while `struct Wrapper { s: String }` needs none?
2. `struct Report<'a> { title: String, body: Excerpt<'a> }` — what must be true to create and keep a `Report`?
3. You keep returning borrowed structs from functions, and each API layer must add `<'a>`. What is the standard fix?
4. What is `Cow<'a, str>` and when does it allocate?
Flashcards
Question
Why does a struct with a `&str` field require `struct S<'a>`?
Click to reveal answer
Answer
A struct value can outlive the function call that created it, so a stored borrow must name the owner span it depends on; the parameter ties every use of the type to data that must still be alive. Function-signature elision never applies to field types.
Question
Which fields need the struct's lifetime parameter, and which never do?
Click to reveal answer
Answer
Only fields that contain borrows — `&'a T`, `&'a mut T`, or wrappers like `Cow<'a, str>` — use the parameter. Owned fields (`String`, `Vec<T>`, `Box<str>`, integers, and owned collections of those) carry no borrowed span and are declared without it.
Question
How does a borrow parameter propagate when structs nest?
Click to reveal answer
Answer
`struct Report<'a> { body: Excerpt<'a> }` reuses the inner struct's parameter, so `Report` becomes a borrowing type too and every layer above it must keep the original data alive. This upward infection is why teams often convert leaf fields to owned `String` at API boundaries.
Question
Compare the three designs: `&'a str` field, `String` field, `Cow<'a, str>` field.
Click to reveal answer
Answer
`&'a str`: zero allocation, but the type carries a lifetime everywhere it flows. `String`: one allocation, and the type is lifetime-free, freely storable and sendable. `Cow<'a, str>`: borrows when unchanged and allocates only when the value must be transformed — the ergonomic middle for library code, but still parameterized by `'a`.
Question
Can you reassign a borrowed field to a shorter-lived source?
Click to reveal answer
Answer
No. The struct's parameter is fixed for the struct's lifetime, so a new value must satisfy the declared `'a` — reassigning to a borrow that ends sooner is rejected. Repointing requires the new source to live at least as long as the parameter, or converting the field to owned data first.
Revision Notes
Key Takeaways
- 1. Struct fields never get lifetime elision — a stored borrow forces a parameter on the definition, because the value outlives any single call
- 2. Only reference-bearing fields use the parameter; owned fields like `String` are declared plainly and impose no outlives obligations
- 3. Borrowed fields propagate: every struct that stores an `Excerpt<'a>` becomes a borrowing type and must keep the original alive
- 4. `Cow<'a, str>` is the middle path — borrowed when unchanged, owned when transformed, uniform through `Deref`
- 5. Converting a field to owned data is the standard repair when lifetime parameters start infecting whole APIs
Interview Tips
- • "Why can't a struct hold `&str` without a lifetime?" — emphasize that structs have no per-call boundary, so the field's validity must be expressible at any later use; elision covers signatures only
- • "Walk through this nesting: `struct A<'a> { b: B<'a> }`" — show that the parameter propagates upward and state the outlives obligation on the original owner
- • "Owned or borrowed field?" — give the decision rule: short-lived per-request views borrow; anything that escapes its source (cache, log, message, thread payload) owns; `Cow` when writing library transform functions
- • "How do you return a reference-backed struct from a function that reads a local?" — you cannot; either take the owner as a parameter, or call `to_owned()` and return an owning type
Cheat Sheet
Holding References in Structs
The rule: fields are not signatures — a stored borrow must be declared:
struct Excerpt<'a> { part: &'a str }
fn first_sentence(text: &str) -> Excerpt<'_> { Excerpt { part: &text[..n] } }
Outlives obligation: the owner of the borrowed data must outlive every use of the struct; 'static sources (literals) satisfy any parameter.
Field audit:
| Field type | Parameter needed? |
|---|---|
&'a str, &'a mut T |
yes |
Cow<'a, str> |
yes |
String, Box<str>, Vec<T> |
no |
u32, bool, owned maps/sets |
no |
Design ladder:
| Field | Cost | Consequence |
|---|---|---|
&'a str |
none | lifetime infects every containing type |
String |
one alloc at build | type composes freely, no annotations upstream |
Cow<'a, str> |
alloc only if transformed | borrow-or-own behind one Deref<Target = str> API |
Escape hatch: field.to_owned() severs the borrow and returns owned data — the standard fix when a function must return data whose source is a local.
Nesting: inner parameters propagate outward (Report<'a> { body: Excerpt<'a> }), so decide ownership at the leaf.