RFA-062 · Case file with fixtures · Case 34 of 694 · Compiler evidence
E0521: When a Typed Closure Parameter Makes a Borrow Escape
An explicit closure reference parameter introduces a lifetime contract that can be more general than the inferred closure needs. Remove the annotation as a test, then decide whether stored data should be borrowed or owned.
- Reviewed
- Rust
- Rust 1.98.1
- Targets
- all targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- The parameter annotation introduces an independently quantified reference lifetime, while storing the value outside the closure requires a relationship the annotation does not provide.
- First discriminating check
- Remove only the closure parameter annotation and let the surrounding vector and call sites infer the reference relationship before redesigning ownership.
More type annotation does not always give Rust more useful information. This compact closure demonstrates the opposite:
fn main() {
let mut outside: Vec<&str> = Vec::new();
let mut push = |value: &str| outside.push(value);
push("atlas");
}
On Rust 1.98.1, outside.push(value) receives E0521:
borrowed data escapes outside of closure
The failing fixture uses a string literal at the call, but the closure's declared contract must work for more than that one literal. The explicit parameter annotation is the important part.
The annotation introduces a reference lifetime
Writing |value: &str| introduces a fresh lifetime for the closure parameter. The closure must be callable with an appropriate borrowed string under that declared input relationship. Its body then tries to store value in outside, a vector which exists beyond one closure invocation.
An arbitrary reference accepted for a call cannot be stored into longer-lived captured state. It might point to a short local owned only by the caller of that invocation. Allowing the push would let the vector retain a dangling reference after that local was dropped.
The compiler note also mentions invariance through &mut Vec<&str>. Mutation cannot safely widen or narrow the vector's reference element lifetime behind a mutable reference, because later code could both write and read values under the original type.
The Rust Reference closure section describes closure parameters, capture modes, and call traits. Here the surprising behavior comes from the relationship between an explicit input reference and captured mutable storage.
Remove only the annotation first
The official diagnostic suggests letting Rust infer the parameter:
let mut outside: Vec<&str> = Vec::new();
{
let mut push = |value| outside.push(value);
push("atlas");
}
Inference can connect the closure input to the particular element lifetime required by outside, rather than promising an independently general &str input. The inner block ends the closure's mutable capture before the assertion in the repaired fixture. That program compiles and runs on Rust 1.98.1.
This is a discriminating repair, not a universal instruction to remove types. It proves that the explicit closure parameter contract was more general than the stored-reference relationship allowed.
Owned storage is often the real production design
If push should accept strings from many unrelated call sites and retain them, the vector should own the data:
let mut outside: Vec<String> = Vec::new();
let mut push = |value: &str| outside.push(value.to_owned());
Now every invocation may provide a short-lived reference. The closure copies the text into an owned String whose lifetime is controlled by the vector. The allocation is not a compiler workaround; it is the cost of retaining data independently from its caller.
Another API can accept String and move it in, avoiding a copy when callers already own their text:
let mut push = |value: String| outside.push(value);
I choose between borrowed and owned storage from the lifetime of the underlying data, not from which version has fewer characters.
Why the literal hides the danger
"atlas" is a &'static str, so this one call could safely be stored forever. But the annotated closure is not automatically restricted to static strings. It says &str, and a later call could pass a slice of a local String.
If static input is truly the intended interface, I can state |value: &'static str|. This works only for literals and other genuinely static text. It is normally too restrictive for collected application input.
Equivalent named functions expose the design
When the closure signature remains confusing, I write a named helper with explicit lifetimes. One form stores references sharing one outer lifetime:
fn push<'a>(outside: &mut Vec<&'a str>, value: &'a str) {
outside.push(value);
}
Now the relationship is visible: the value must be valid for the vector's element lifetime. A different, higher-ranked function that accepts a reference of any lifetime could not store it in one fixed outer vector.
My debugging rule
For E0521 in a closure, I inspect three boundaries:
- Which values are captured, and whether the capture is mutable.
- Whether the parameter reference lifetime is inferred or explicitly introduced.
- Whether the body returns or stores that reference outside one invocation.
Removing an annotation can confirm the mechanism, but it cannot make genuinely short-lived data safe to retain. The final choice is still between a correctly related borrowed lifetime and owned data.
The official E0521 page presents this same core shape. The deeper lesson is that &str is not one universal reference type with no time attached. A closure parameter annotation states a calling contract, and storing its value creates a second contract. Rust requires those two to agree.