Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-608 · Case file with fixtures · Case 580 of 694 · Compiler evidence

Rust Opaque Return Types Must Declare Captured Lifetimes

An opaque return hides its concrete type, not the lifetimes stored inside it. Declare every captured lifetime under the edition's capture rules.

Reviewed
Rust
Rust 1.98.1, edition 2021
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Opaque syntax hid concrete type identity but was assumed to hide the lifetime of a reference physically stored inside that type.
First discriminating check
Inspect the hidden type and edition capture rules, then declare the real lifetime capture precisely and avoid unrelated over-capture.

Return-position impl Trait hides a concrete type from callers, but the hidden type still stores whatever references its fields contain. In edition 2021, the failing fixture returns Cell<&'data u32> without mentioning 'data in the opaque bounds, producing E0700.

Opaque does not mean independent of inputs

The return type tells callers only that one hidden concrete type implements View<'view>. The body chooses that concrete type. Because it contains &'data u32, a returned value cannot outlive the data borrow.

The official E0700 explanation describes the missing capture and shows adding the lifetime to the opaque bounds. This makes the ownership limitation visible even while representation stays hidden.

I trace the actual hidden type, including closure captures, iterator adapters, futures, and nested structs. A reference can be buried several layers below the return expression.

The repair states the capture in edition 2021

The repaired fixture returns impl View<'view> + 'data. That tells callers the opaque value captures data constrained by 'data.

Modern Rust also supports precise capture syntax in relevant positions, and edition 2024 changed default return-position opaque lifetime capture rules. The Edition Guide explains the migration. This fixture deliberately pins edition 2021 because the diagnostic and repair depend on those rules.

I test the project's minimum Rust version and edition rather than copying syntax that only works in a newer toolchain.

Over-capture can restrict callers

Capturing more lifetimes than the hidden type needs can make a returned value appear tied to an input unnecessarily. This is especially visible with Rust 2024's broader automatic capture. Precise capture syntax can document the exact generic parameters used.

The Reference covers automatic capturing. I distinguish language capture rules from actual borrow data flow and verify both through compile tests.

For example, a function may accept a configuration reference while returning an iterator over owned data. If the opaque type does not retain configuration, callers should not lose the ability to use the iterator after that borrow ends.

Async functions return opaque futures

An async fn effectively returns a hidden future type that captures inputs needed across awaits. Lifetime capture errors and unexpectedly long borrows often reflect fields stored in that generated state machine.

I inspect which values are used after suspension and move ownership into the future when it should be independent. Boxing changes storage and dispatch but does not erase borrowed lifetime requirements.

Closures and iterator chains work similarly: laziness means a reference may be stored for later rather than consumed during construction. Returning a collected owned container can remove the capture at an allocation cost.

Public opaque APIs need negative tests

The hidden concrete type may change without breaking callers, which is a major benefit. Capture bounds remain part of usability. An internal refactor can accidentally make a borrow last longer even while the written return type seems stable.

I add compile-pass cases showing the result works for intended scopes and compile-fail cases showing it cannot outlive real backing data. These tests catch capture changes that runtime tests cannot observe.

Edition migrations deserve a dedicated review of opaque returns, async functions, and use<...> suggestions. A clean compilation is not proof that broader capture has no API effect.

I record the edition in compile evidence for this reason. The same source-level signature may receive different capture interpretation after migration, so comparison must keep toolchain, edition, and caller scope together.

My E0700 checklist

  • What concrete hidden type does the function body return?
  • Which references, closures, iterators, or futures does that type store?
  • Which edition and MSRV capture rules apply?
  • Should the missing lifetime appear as an outlives bound or precise capture list?
  • Is the type capturing unrelated inputs longer than necessary?
  • Would owned output remove a harmful caller-lifetime coupling?
  • Do async suspension points retain the expected inputs?
  • Are compile-pass and compile-fail scope tests protecting the public contract?

The core principle is that hiding a type does not hide its need for valid storage. E0700 exposes a missing lifetime dependency. I declare the real capture, then minimise it so opaque APIs preserve both safety and useful caller freedom.