Rust impl Trait makes borrowing an interface decision
Rust 1.82 precise capture bounds separate hidden-type borrowing from outlives promises. Review the API contract, edition rules and limits before changing it.
A function that returns an opaque iterator still owes its caller an account of what it borrows. Rust 1.82 made that account easier to express with precise capture bounds. The useful review question is whether the signature describes the dependencies of the returned value accurately. Adding a stronger lifetime relationship simply to remove a diagnostic can make an otherwise useful interface unnecessarily restrictive.
This is a retrospective written in November 2026. The stable release on 17 October 2024 introduced use<..> for certain return-position impl Trait types. It did not stabilize the Rust 2024 edition. Keeping those milestones separate matters when a library promises to support a particular compiler and edition.
What the caller needs to know
An opaque return type hides the concrete implementation while exposing selected traits. That is useful when callers need iteration rather than the name of a complicated iterator type. Hiding the type cannot hide its borrowing obligations: callers still need to know which inputs constrain how they may use the result.
The language team's 5 September 2024 design explanation describes capture as permission for the hidden type to use a generic parameter. A capture bound identifies that permission. An outlives bound makes a different promise about how long the returned type remains valid. Confusing those statements can introduce a relationship between input lifetimes that the operation never required.
For a maintainer, the consequence appears in calling code. A caller may have a short-lived context and independently borrowed data. If the signature accidentally connects those lifetimes, the caller may have to restructure otherwise valid work. That is an interface cost even when the implementation looks compact and the compiler diagnostic has disappeared.
The counterexample that earns the syntax
The October release notes give an iterator example with a context lifetime 'cx and a data lifetime 'a. The iterator captures the context for logging and yields a reference to the data. Using + 'cx to make the context lifetime visible also imposes an outlives requirement; repairing that by demanding 'a: 'cx would relate two lifetimes that need not have that relationship. The notes replace it with use<'cx, 'a>.
That example establishes a narrow distinction. Capturing both lifetimes permits the hidden type to depend on both. It does not require the data lifetime to outlive the context lifetime. A review should explain which references are retained, and for what purpose, before choosing the bound. A new spelling without that explanation can preserve the same conceptual mistake.
There is also a simpler counterexample to a blanket replacement rule. The release explicitly says that the small example returning the borrowed input has equivalent behavior with + '_ and + use<'_>. Existing code using an outlives bound is therefore not automatically wrong. The question is whether the bound states a genuine requirement or an accidental one.
Three dates belong in the review
The September post explained the direction. October supplied stable syntax with restrictions. Rust 1.85, released on 20 February 2025, stabilized the 2024 edition. A team adopting syntax on a 2021-edition crate in October was making a different change from a later edition migration.
At 1.82 stabilization, the explicit capture list had to include all in-scope generic type and const parameters, and the syntax was unavailable inside traits and trait implementations. Those contexts already captured in-scope lifetimes by default. Do not read a present-day example as proof that every placement it uses compiled on 1.82.
The current edition guide, read for this retrospective, explains the later edition default: absent an explicit capture bound, return-position opaque types capture all in-scope generic parameters, including lifetimes. In 2021 and earlier, the old rules for bare functions and inherent methods did not implicitly capture every lifetime. An edition change can therefore alter the interface's apparent dependencies even without a new function body.
More capture can also be too much
The edition guide demonstrates a function that captures a lifetime even though its hidden type does not use it. A caller trying to obtain a 'static opaque result can then be rejected. Capture is permission that affects the public contract; it is not a precise report of what the current body actually stored.
This is why “capture everything” is not a universal API design policy. When a returned value intentionally owns its result and does not depend on a particular input borrow, excluding that lifetime can preserve useful freedom for callers. Conversely, excluding a lifetime the implementation genuinely needs is not a way around borrowing rules. The signature and implementation must agree.
No capture annotation can make a reference to destroyed data valid. Nor does this feature remove unrelated borrow conflicts, repair incorrect ownership or prove that an API is pleasant to use. It addresses one specific part of opaque-type contracts.
Prefer the interface another maintainer can explain
The strongest alternative is a concrete return type. If it is short, stable and communicates the relationship clearly, exposing it may be easier than teaching every maintainer another opaque-type rule. The trade is that callers can become coupled to that type and future implementation changes may affect compatibility. Choose deliberate abstraction where that flexibility has value.
For a library review, record the supported compiler, edition and intended input dependencies together. Ask for caller examples that should remain accepted and examples that should remain rejected; these are proposed verification cases, not tests performed here. Review changes to capture bounds as interface changes rather than cosmetic diagnostic cleanup.
In custom software development, the business reason is maintainability across callers and future releases. A signature that describes the real borrowing relationship gives the next developer a smaller problem to solve. Rust 1.82 supplied a more exact vocabulary for that work; the API author still has to choose the right contract.
Paul