1. Symptoms
When attempting to use a closure that captures a struct containing non-'static references, the Rust compiler produces error E0595 with a message indicating that closures cannot capture structs with borrow checker restrictions.
Common scenarios where this error manifests:
Scenario 1: Struct with self-referential lifetime field
struct Config<'a> {
name: String,
reference: &'a str,
}
fn main() {
let config = Config {
name: String::from("app"),
reference: "value",
};
let closure = || {
println!("{}", config.reference);
};
closure();
}
---
The compiler reports:
error[E0595]: closures cannot capture structs with borrow checker restrictions –> src/main.rs:10:17 | 10 | let closure = || { | —–^^^^^ | = note: closure may only be used as or returned from the closure’s environment, so it cannot be stored in a struct or returned from a function
### Scenario 2: Closure returned from function with borrowed struct
```rust
struct Context<'a> {
data: &'a [u8],
}
fn create_processor<'a>(ctx: &'a Context) -> impl Fn() -> usize + 'a {
|| ctx.data.len() // E0595 here
}
Scenario 3: Attempting to store closure in struct field
struct Handler<'a> {
callback: Box<dyn Fn() + 'a>,
}
struct State<'a> {
value: String,
handler: Handler<'a>,
}
Error output varies slightly based on context, but the core message remains consistent: the closure cannot capture the struct due to borrow checker restrictions.
2. Root Cause
The E0595 error stems from a fundamental limitation in Rust’s borrow checker regarding how closures interact with structs containing non-'static lifetimes.
The Core Problem
When you have a struct containing a reference with a lifetime tied to that struct’s own lifetime parameter, the borrow checker cannot determine how to manage the captured environment. Here’s why:
-
Self-referential lifetime: When a struct like
Config<'a>contains a fieldreference: &'a str, the lifetime'amust outlive theConfiginstance. -
Closure capture semantics: Closures capture variables from their environment by moving or borrowing them. The captured environment becomes part of the closure’s type.
-
Lifetime conflict: When a closure captures a
Config<'a>where'ais tied to the struct’s lifetime, the compiler cannot determine whether the captured reference should extend beyond the closure itself.
Why This Is Problematic
Consider what would happen if this were allowed:
struct Broken<'a> {
data: &'a str,
}
fn create_closure<'a>(x: &'a str) -> Box<dyn Fn() -> &'a str + 'a> {
let broken = Broken { data: x };
// If we return this closure, what happens to 'broken'?
// The closure captures 'broken', but 'broken' contains 'x'
// The returned closure contains a reference to something that may not exist
Box::new(|| broken.data)
}
The borrow checker prevents this because the lifetimes don’t align correctly. The struct Broken is created locally, and its lifetime is tied to create_closure’s execution. But the closure tries to capture it and return itself.
The Borrow Checker Restrictions
The specific restrictions that trigger E0595:
- Struct contains a reference with a lifetime parameter
- That lifetime is constrained by the struct’s own lifetime parameter
- A closure attempts to capture an instance of that struct
- The closure would outlive the struct’s scope
The compiler’s error message clarifies: “closures cannot capture structs with borrow checker restrictions” because “closure may only be used as or returned from the closure’s environment, so it cannot be stored in a struct or returned from a function.”
3. Step-by-Step Fix
The solution to E0595 depends on your use case. Here are the primary approaches:
Fix 1: Use owned data instead of references
Before:
struct Config<'a> {
name: String,
reference: &'a str,
}
fn process(config: Config) {
let closure = || println!("{}", config.reference);
closure();
}
After:
// Instead of storing a reference, store owned data
struct Config {
name: String,
reference: String, // Owned instead of borrowed
}
fn process(config: Config) {
let closure = || println!("{}", config.reference);
closure();
}
fn main() {
let config = Config {
name: String::from("app"),
reference: String::from("value"),
};
process(config);
}
Fix 2: Return the closure’s output instead of the closure itself
Before:
struct Context<'a> {
data: &'a [u8],
}
fn create_processor<'a>(ctx: &'a Context) -> impl Fn() -> usize + 'a {
|| ctx.data.len() // Error: captures 'ctx' which has restricted lifetime
}
After:
// Return a function that takes the context as a parameter
fn create_processor() -> impl Fn(&[u8]) -> usize {
|data| data.len()
}
fn main() {
let ctx = vec![1, 2, 3, 4, 5];
let processor = create_processor();
// Now you can use the closure with data from any source
let result = processor(&ctx);
println!("Length: {}", result);
}
Fix 3: Use higher-ranked trait bounds (HRTB)
Before:
struct Config<'a> {
value: &'a str,
}
fn call_with_config<'a>(config: &'a Config, f: impl Fn(&Config)) {
f(config);
}
After:
struct Config {
value: String, // Use owned type
}
fn call_with_config(f: impl for<'a> Fn(&'a Config)) {
// Higher-ranked trait bounds allow the function to accept
// closures with any lifetime
}
// Or with a concrete wrapper
struct Processor {
data: String,
}
impl Processor {
fn execute(&self, f: impl Fn(&str)) {
f(&self.data);
}
}
fn main() {
let processor = Processor {
data: String::from("test data"),
};
processor.execute(|s| println!("{}", s));
}
Fix 4: Move the data into the closure
Before:
struct Parser<'a> {
input: &'a str,
offset: usize,
}
fn create_parser(input: &str) -> impl Fn() -> Option<char> {
let parser = Parser { input, offset: 0 };
move || { // E0595 still occurs here
if parser.offset < parser.input.len() {
let ch = parser.input[parser.offset..].chars().next()?;
parser.offset += ch.len_utf8();
Some(ch)
} else {
None
}
}
}
After:
struct Parser {
input: String, // Owned
offset: usize,
}
fn create_parser(input: String) -> impl Fn() -> Option<char> {
let mut parser = Parser {
input,
offset: 0,
};
move || {
if parser.offset < parser.input.len() {
let ch = parser.input[parser.offset..].chars().next()?;
parser.offset += ch.len_utf8();
Some(ch)
} else {
None
}
}
}
fn main() {
let parser = create_parser(String::from("hello world"));
while let Some(ch) = parser() {
println!("{}", ch);
}
}
Fix 5: Use Rc for shared ownership
Before:
struct SharedData<'a> {
value: &'a str,
}
fn create_sharer<'a>(data: &'a str) -> impl Fn() -> &'a str {
|| data // E0595
}
After:
use std::rc::Rc;
fn create_sharer(data: String) -> impl Fn() -> String {
let data = Rc::new(data);
move || (*data).clone()
}
fn main() {
let sharer = create_sharer(String::from("shared data"));
println!("{}", sharer());
println!("{}", sharer());
}
4. Verification
After applying your chosen fix, verify it compiles correctly:
Test Compilation
rustc --edition 2021 your_file.rs
# Or with cargo
cargo build
Verify Runtime Behavior
Create a test to ensure your fix works correctly at runtime:
#[cfg(test)]
mod tests {
#[test]
fn closure_produces_correct_output() {
// Test case for Fix 1
struct Config {
name: String,
reference: String,
}
let config = Config {
name: String::from("test"),
reference: String::from("expected"),
};
let closure = || config.reference.clone();
assert_eq!(closure(), "expected");
}
#[test]
fn processor_handles_data() {
// Test case for Fix 2
fn create_processor() -> impl Fn(&[u8]) -> usize {
|data| data.len()
}
let processor = create_processor();
let test_data = vec![1, 2, 3, 4, 5];
assert_eq!(processor(&test_data), 5);
}
}
Run Tests
cargo test
Check for Regression
Ensure no other parts of your codebase are affected:
cargo build --all-targets
cargo test --all-targets
5. Common Pitfalls
Pitfall 1: Confusing &'a str with String
Beginners often use string references where owned strings are needed:
// This will fail
fn broken() -> impl Fn() -> &'static str {
let s = String::from("hello");
|| &s // s doesn't live long enough
}
// This works
fn fixed() -> impl Fn() -> String {
let s = String::from("hello");
move || s.clone() // Clone the owned data
}
Pitfall 2: Assuming impl Fn() + 'a will work
Just adding a lifetime bound doesn’t solve the problem:
// Still broken
struct Wrapper<'a> {
data: &'a str,
}
fn create_wrapper<'a>(s: &'a str) -> impl Fn() -> &'a str + 'a {
|| s // E0595 persists
}
Pitfall 3: Forgetting that closures capture by reference by default
struct Data {
value: i32,
}
fn main() {
let data = Data { value: 42 };
let closure = || data.value; // Captures by reference
// data is moved here if uncommented:
// drop(data);
// closure(); // Would fail because data was moved
println!("{}", closure());
}
Pitfall 4: Returning closures from functions with incorrect trait bounds
// Incorrect
fn bad_return() -> impl Fn() {
let x = 10;
|| x
}
// Correct - closure can capture owned data
fn good_return() -> impl Fn() -> i32 {
let x = 10;
move || x
}
Pitfall 5: Nested lifetimes in complex structs
// Complex case that requires careful handling
struct Inner<'a> {
data: &'a str,
}
struct Outer<'a, T: 'a> {
inner: &'a Inner<'a>,
processor: fn(&Inner<'a>) -> T,
}
// Simplify to avoid E0595
struct SimpleOuter {
inner: Inner,
}
6. Related Errors
Understanding E0595 becomes easier when you know these related errors:
E0501: Cannot borrow as mutable because it is also borrowed as immutable
Occurs when you have overlapping borrows of the same data:
let mut data = vec![1, 2, 3];
let first = &data[0]; // Immutable borrow
data.push(4); // Mutable borrow - conflicts with first
println!("{}", first); // Uses the immutable borrow
E0502: Cannot borrow x as mutable because it is also borrowed as immutable
A specific case of E0501 that appears frequently with closures:
let mut v = vec![1, 2, 3];
let handle = v.get(0); // Immutable borrow
v.push(4); // Mutable borrow conflicts
E0591: Cannot infer an appropriate lifetime
When the compiler cannot determine lifetimes:
struct Ref<'a> {
value: &'a i32,
}
fn create() -> Ref<'static> {
let x = 5;
Ref { value: &x } // x doesn't live long enough
}
E0621: Explicit lifetime annotation required
When function return types need explicit lifetime annotations:
struct Context<'a> {
data: &'a str,
}
fn get_context() -> Context<'_> {
let s = String::from("data");
Context { data: &s } // Missing lifetime annotation
}
E0507: Cannot move out of self because it is behind a reference
When you try to move data out of a borrowed reference:
struct Wrapper {
data: String,
}
impl Wrapper {
fn extract(self: &Wrapper) -> String {
self.data // Cannot move out of borrowed content
}
}
E0505: Cannot move out of captured variable in an Fn context
When an Fn closure tries to move captured data:
fn main() {
let data = String::from("hello");
let closure = || drop(data); // Cannot move from Fn context
}
The common thread among these errors is the borrow checker’s strict rules about how data can be accessed and moved. E0595 specifically addresses the case where a closure would need to capture a struct with borrow checker restrictionsβtypically structs containing references with lifetimes tied to the struct itself.
Quick Reference: Error Differentiation
| Error | Problem | Solution |
|---|---|---|
| E0595 | Closure captures struct with lifetime-linked references | Use owned data, HRTB, or change architecture |
| E0501 | Conflicting borrows | Reorder operations, use indices |
| E0502 | Mutable borrow during immutable borrow | Ensure exclusive access |
| E0591 | Cannot infer lifetime | Add explicit lifetime annotations |
| E0621 | Missing lifetime in return | Annotate lifetimes properly |