Fix E0595: Closures Cannot Capture Structs with Borrow Checker Restrictions

Rust intermediate Linux macOS Windows WebAssembly

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:

  1. Self-referential lifetime: When a struct like Config<'a> contains a field reference: &'a str, the lifetime 'a must outlive the Config instance.

  2. Closure capture semantics: Closures capture variables from their environment by moving or borrowing them. The captured environment becomes part of the closure’s type.

  3. Lifetime conflict: When a closure captures a Config<'a> where 'a is 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,
}

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