RFA-480 · Case file with fixtures · Case 452 of 694 · Compiler evidence
Nested Rust Functions Cannot Capture Local Values
Lexically nested fn items remain independent functions. Use a closure when capture is intentional, pass data as explicit parameters for reusable logic, or use a true const only when the value is compile-time global.
- 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
- Lexical nesting limits visibility but a fn item has no generated environment storing locals from one invocation of its enclosing function.
- First discriminating check
- Use a closure for intentional capture or pass the dependency as an explicit parameter when the helper should remain an independent reusable function.
I declared fn inner inside another function and read the outer local offset. Rust emitted E0434 because a nested function item cannot capture its dynamic environment.
The failing fixture is visually nested, but lexical placement does not turn the inner fn into a closure. It remains an independently callable item with only its declared parameters.
A function item has no hidden environment
The function Reference defines a function by its explicit signature and body. A nested item may use module items and generic context according to item rules, but it does not receive outer stack locals silently.
offset exists each time outer runs. A plain function item has no field or hidden pointer telling it which invocation's offset to read.
E0434 prevents that unresolved runtime relationship.
Use a closure when capture is part of the operation
The repaired fixture writes:
let inner = |value: u8| value + offset;
The closure value carries access to offset according to how its body uses it. The closure Reference describes capture modes and the Fn, FnMut, and FnOnce call traits.
This is honest when the helper exists only for this invocation and depends on its local context.
Pass the value explicitly for reusable logic
If the calculation deserves a normal function, I write fn inner(value: u8, offset: u8) -> u8. The dependency then appears in tests, call sites, and documentation.
Explicit parameters often make extraction to module scope straightforward. They also avoid a closure borrowing large state longer than expected.
I choose this form when the helper is reusable, pure, or part of a stable processing pipeline.
Capture mode follows usage
A closure that only reads may borrow shared state. Mutation may require a mutable capture. move can transfer ownership or copy captured values into the closure environment.
Adding move is not a general lifetime fix: references captured by value remain references with their original lifetimes. I inspect the closure's actual captured types, especially before spawning threads or storing callbacks.
For async blocks, similar capture and lifetime reasoning applies to the generated future state.
Constants are not substitutes for per-call data
The E0434 explanation notes that constants or statics can be used by nested functions because they are items rather than dynamic locals.
I use a const only when offset is genuinely one compile-time value for all calls. Promoting configuration or request data to a global static changes concurrency, test isolation, and API semantics.
Mutable statics introduce unsafe access and are almost never the right response to a simple capture error.
Nested location can still be useful
A nested fn can keep a helper private to one enclosing body and avoid module-level naming, even though it does not capture. This can be suitable for a recursive helper whose entire state is passed explicitly.
However, heavy nested items can make code navigation harder. I move the function to module scope when it has independent tests or a meaningful domain name.
Scope should serve readability, not imitate closures.
Call-trait requirements influence the repair
An API may accept a function pointer fn(u8) -> u8, which non-capturing closures can coerce to, but a capturing closure cannot because it needs an environment. Such APIs may instead be generic over F: Fn(u8) -> u8 or accept a trait object.
I do not erase capture with global state to meet a function-pointer signature. I decide whether the callback contract should support stateful behaviour.
My E0434 checklist
I also look at testability. A closure tied to a large outer scope can be difficult to exercise independently, while an explicit helper with a small context struct makes inputs reproducible. If capture grows beyond two or three clearly related values, I often package the state under a name and pass or borrow that context. This keeps the convenience of local composition without making the callable's dependencies invisible. The shape of the environment is part of the design even though the compiler can synthesize it.
- Which outer local does the nested function try to use?
- Is per-invocation capture genuinely intended?
- Would an explicit parameter make the dependency clearer?
- Does the closure borrow, mutate, copy, or move the value?
- Must the callback outlive the current stack frame?
- Is the value truly constant across all calls?
- Does the receiving API require a function pointer or allow
Fn? - Should the helper move to module scope for reuse and testing?
The core principle is that lexical nesting does not create an implicit runtime environment for a function item. I use closures for captured context and explicit parameters for independent functions, keeping ownership visible in either form.