July 12, 2025•3 min

How do you handle lifetimes when returning a closure that captures variables from its environment?

m
mayo

When returning a closure that captures variables (especially references), you must ensure the captured data outlives the closure. Rust enforces this through lifetime annotations and ownership rules. Here's how to handle it:

Key Strategies

Use move to Transfer Ownership

Force the closure to take ownership of captured variables, eliminating dependency on external lifetimes:

fn create_closure() -> impl Fn() -> String {
    let s = String::from("hello"); // Owned data
    move || s.clone() // `move` captures `s` by value
}

Annotate Lifetimes for Captured References

If capturing references, explicitly tie the closure's lifetime to the input data:

fn capture_ref<'a>(s: &'a str) -> impl Fn() -> &'a str {
    move || s // Closure's output tied to `'a`
}

Avoid Returning Closures Capturing Short-Lived References

Closures capturing references to local variables cannot escape their scope:

// ERROR: `s` does not live long enough!
fn invalid_closure() -> impl Fn() -> &str {
    let s = String::from("hello");
    move || &s // `s` dies at end of function
}

Example: Safe Lifetime Management

// Correct: Closure owns the captured data
fn safe_closure() -> impl Fn() -> String {
    let s = String::from("hello");
    move || s // `s` is moved into the closure (owned)
}

// Correct: Closure tied to input reference's lifetime
fn capture_with_lifetime<'a>(s: &'a str) -> impl Fn() -> &'a str + 'a {
    move || s // Closure's lifetime matches `s`
}

Lifetime Pitfalls

Dangling References

Returning a closure that captures a reference to a local variable will fail:

fn dangling_closure() -> impl Fn() -> &str {
    let local = String::from("oops");
    move || &local // ERROR: `local` dies here
}

Elision Ambiguity

Use explicit lifetimes when the compiler can't infer relationships:

// Explicitly annotate input and closure lifetimes
fn process<'a>(data: &'a [i32]) -> impl Fn(usize) -> &'a i32 + 'a {
    move |i| &data[i] // Closure tied to `data`'s lifetime
}

Key Takeaways

āœ… Use move to transfer ownership of captured variables. āœ… Annotate lifetimes when closures capture references. 🚫 Avoid returning closures that capture short-lived references.

Real-World Use Case

In web frameworks like actix-web, handlers often return closures capturing request data with explicitly managed lifetimes.

Try This: What happens if you remove move from capture_with_lifetime?

Answer: Compiler error! The closure would try to borrow s, which doesn't live long enough.