RFA-574 · Case file with fixtures · Case 546 of 694 · Compiler evidence
A Rust Borrow Cannot Outlive the Local Value Backing It
A reference does not carry its referent's storage with it. Move the owner to the required scope, return ownership, or shorten the borrow.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The reference binding has wider lexical scope than the allocation it borrows, and moving a view does not carry or extend its owner.
- First discriminating check
- Identify the concrete owner and last use, then widen ownership, return owned storage, or keep the borrow and its use inside the shorter scope.
A &str is a view into text stored somewhere else. Moving that view into a wider variable does not move or extend the underlying allocation. When the local String owning the bytes reaches the end of its block, the reference cannot remain usable, so rustc reports E0597.
The failing fixture declares selected outside an inner block. Inside, it builds response and assigns response.as_str() to selected. The later print would read storage after its owner was dropped.
Scope of the binding is not lifetime of the data
The destination variable is in scope long enough, but the referent is not. This distinction is one of the most reusable Rust debugging principles. I draw two separate lines: where the reference variable may be named, and where the owned allocation remains valid. The usable borrow ends at the shorter line.
The E0597 explanation uses the same underlying rule: a value goes out of scope while something still borrows it. Rust rejects the program before the dangling reference exists.
Adding a lifetime annotation cannot make response live longer. Lifetimes describe and constrain relationships already supported by ownership; they do not delay destruction or allocate storage.
Move the owner, own the result, or shorten the use
The repaired fixture moves response to the enclosing scope. Now its allocation remains alive through the assertion that uses selected.
That is one of three common repairs. If the wider operation naturally owns the response, widen the owner's scope. If the data must cross a function, task, queue, or cache boundary independently, return or store an owned String instead. If the view is needed only for parsing inside the block, keep the borrow and its use local.
I choose from domain ownership rather than trying random clones. Cloning into a new String is correct when a new independent owner is required. It is wasteful and misleading when reorganising scopes or returning the existing owner would express the real transfer.
Returned references need an input source
The same failure appears when a function creates a String and tries to return &str into it. Local storage is destroyed when the function returns. A borrowed output normally needs to point into borrowed input, static storage, or data owned by an object that outlives the call.
If a parser receives &str and returns a slice from it, the lifetime relationship is meaningful. If a loader creates text, returning String gives ownership to the caller. If many results share immutable text, an Arc<str> may represent that shared ownership. Each type says who keeps bytes alive.
Temporary values create a related trap. Borrowing from a temporary and saving the reference beyond the temporary's destruction scope can produce E0597 or a neighbouring diagnostic. The Reference documents destructor scopes, but I usually simplify the code by naming the owner and making its required lifetime visible.
Async and threads make ownership boundaries sharper
A spawned task may outlive the stack frame that created it. APIs often require a 'static future or closure, which means it cannot borrow short-lived locals, not that every referenced value must exist forever. Moving owned values or Arc handles into the task gives it a self-contained lifetime.
I avoid responding to such errors by leaking memory to obtain a 'static reference. Leaking is rarely the domain ownership model. An owned message, scoped task, or shared owner is normally the honest repair.
For caches and structs that store references, I first ask who owns the backing data and who controls eviction. If that answer is unclear, a lifetime annotation will only encode confusion.
My E0597 checklist
- Which concrete allocation or stack value does the reference point into?
- At what exact scope is that owner destroyed?
- Where is the reference last used?
- Should the wider operation own the original value?
- Should the result cross the boundary as an owned
Stringor shared owner? - Can parsing and use remain inside the owner's scope?
- Is a clone creating a necessary owner or only hiding the design?
- For a task or stored callback, who guarantees the captured data remains alive?
The core principle is that references borrow validity; they do not transport storage. I solve E0597 by making ownership match the duration of the real operation. Once the owner is clear, the lifetime usually becomes simple enough for Rust to infer and for another engineer to review.