RFA-588 · Case file with fixtures · Case 560 of 694 · Compiler evidence
Rust Returned Borrow Lifetimes Must Match Every Possible Source
A borrowed return contract must cover every branch that can supply the reference. Align signature and data flow, or change ownership and selection design.
- 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
- One conditional return source has an independent possibly shorter borrow that the function's public output relationship does not cover.
- First discriminating check
- Trace every return branch to its owner, then connect all possible sources to the output or change the body or ownership contract.
A lifetime annotation is a promise about where a returned reference may come from. The failing fixture declares that larger returns a reference with left's lifetime, but one branch returns right. Rust reports E0621 because signature and body data flow disagree.
Read lifetime names as relationships
The signature fn larger<'a>(left: &'a i32, right: &i32) -> &'a i32 says the output remains valid for the chosen 'a associated with left. The elided lifetime of right is independent. It might be shorter, so returning it under the 'a promise would let a caller keep a reference after right is gone.
The official E0621 explanation describes exactly this mismatch. Rust is not demanding more annotations for their own sake. It has found a return path that the existing annotation does not permit.
I trace each return, tail expression, match arm, and conditional branch back to its borrowed source. A signature must honestly cover all of them.
The repair gives both candidates one usable lifetime
The repaired fixture attaches 'a to both parameters and the result. At a call, the usable shared lifetime becomes no longer than the shorter input borrow. That is exactly what selection between two sources requires.
This does not force the owned integers to have identical total lifetimes. It constrains the particular borrows used for the call and result. The caller cannot use the chosen reference beyond either candidate's relevant availability.
An alternative repair is to change the body so it always returns left. Then right need not share 'a. Which repair is correct depends on the function's behaviour, not on which diff is shorter.
One lifetime name can be too restrictive elsewhere
Connecting inputs is correct only when output may borrow either. If a method returns a slice solely from self, giving an unrelated configuration argument the same lifetime unnecessarily prevents valid calls. I keep independent inputs independent unless data flow connects them.
The Book's lifetime chapter frames annotations as relationships rather than duration commands. This prevents the common reaction of adding one lifetime name everywhere until code compiles.
When output combines views from multiple owners, a single borrowed return may not fit. Returning owned data, an enum preserving which owner is borrowed, or performing work inside a callback can express the real operation without impossible lifetime promises.
Branches hidden in helpers still matter
The returned source may be selected through iterator combinators, helper functions, or methods. I simplify the failing code temporarily and annotate intermediate types. Compiler spans often identify the branch, but the public signature remains the contract to review.
For structs storing references, a constructor may similarly accept independent lifetimes and then place one where another was promised. Naming roles such as 'config, 'request, and 'output during diagnosis is clearer than a long sequence of letters.
Async functions add state-machine storage, but they do not change the core rule. A returned or stored borrow still needs an owner that survives every suspension and later use. Often the right task boundary takes ownership rather than carrying request-local references.
Ownership can simplify an awkward selection API
For Copy values such as the fixture's integers, returning the value rather than a reference is often simpler and cheaper to reason about. For strings, returning Cow, cloning intentionally, or returning an index into caller-owned storage may be appropriate.
I do not optimise references before measuring. A borrowed result saves copying but couples caller lifetime to all possible sources. The API cost can exceed the allocation or copy it avoids.
Tests create inputs in nested scopes to prove the intended restriction. Compile-fail fixtures are especially useful because a runtime test cannot execute a lifetime violation that rustc rejects.
My E0621 checklist
- What lifetime does the output signature currently promise?
- Which input or stored owner can supply the result on every branch?
- Does an elided parameter have an independent, possibly shorter lifetime?
- Should candidate inputs share a call-scoped lifetime?
- Could the body return from only the source named by the current contract?
- Is one lifetime name over-constraining unrelated inputs elsewhere?
- Would returning an owned value make the API more useful and simpler?
- Do compile-fail tests preserve the intended lifetime boundary?
The core principle is that lifetime syntax must describe actual data flow. I do not add annotations to extend storage. I align the public promise with every possible borrowed source, or change the function so ownership better matches what callers need.