July 12, 20255 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:

Move owned data in; never return a closure outliving a captured reference fn f() -> impl Fn() -> ... captures a local variable move || s (owned String) safe: closure owns the data no dependency on caller's frame move || &local ERROR: `local` dropped at fn end compiler rejects the dangling ref

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`
}

That single 'a is doing three jobs at once: it names how long the input borrow is good for, it bounds how long the closure itself may live, and it stamps the reference the closure hands back. All three spans have to fit inside the caller's data:

One `'a`, three nested spans — all must close before the data dies fn capture_ref<'a>(s: &'a str) -> impl Fn() -> &'a str + 'a data dropped here caller's String the only thing that actually owns bytes s: &'a str borrow handed to the function closure + 'a may not be called past this bar value it returns &'a str, same borrow flowing out Drop the `+ 'a` bound and the closure is free to escape past the bar — that is the error.

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.