Skip to content
← All Rust articles

Rust explainer

Helping closures infer reference lifetimes

About 4 min to read. Allow extra time to work through examples.

A closure returning a borrowed value may need an expected callable signature. Learn when to use a function, a function pointer or a higher-ranked bound, with complete examples.

This article uses Rust. You can make it your practice mode to browse related content across the site.

A function that returns its borrowed input can leave the lifetime names out:

fn identity(x: &i32) -> &i32 {
    x
}

fn main() {
    let number = 7;
    assert_eq!(*identity(&number), 7);
}

The signature means fn identity<'a>(x: &'a i32) -> &'a i32: each call accepts a reference and returns one valid for the same lifetime. The caller does not have to supply a reference that lives forever.

A similar-looking closure can fail. This example intentionally does not compile:

fn main() {
    let identity = |x: &i32| x;
    let number = 7;
    assert_eq!(*identity(&number), 7);
}

The diagnostic says that a lifetime may not live long enough. The standalone closure annotation has not established the input/output relationship needed here. Adding -> &i32 to this closure is not enough to resolve it.

This is an inference issue, not evidence that returning a borrowed argument is unsafe. An expected callable signature can give inference the relationship it needs. Elision rules apply to function and callable trait signatures; that does not mean every locally annotated closure expression gets the same inferred signature automatically.

Use a function pointer when nothing is captured

A noncapturing closure can coerce to a function pointer. Annotating the receiving binding supplies a signature with linked input and output lifetimes:

fn main() {
    let identity: fn(&i32) -> &i32 = |x| x;
    let outer = 7;
    assert_eq!(*identity(&outer), 7);
    {
        let inner = 9;
        assert_eq!(*identity(&inner), 9);
    }
    assert_eq!(*identity(&outer), 7);
}

The pointer type can also be written for<'a> fn(&'a i32) -> &'a i32. The for<'a> says that it works for each input lifetime, not one fixed lifetime chosen when the pointer was created. A named function such as the first example is often the clearest choice when there is no environment to capture.

Supply a bound when the closure captures data

A capturing closure cannot coerce to a function pointer. A generic helper can instead describe the required relationship with a higher-ranked trait bound. This helper returns the same closure value it receives:

use std::cell::Cell;

fn linked<F>(f: F) -> F
where
    F: for<'a> Fn(&'a i32) -> &'a i32,
{
    f
}

fn main() {
    let calls = Cell::new(0);
    let identity = linked(|x| {
        calls.set(calls.get() + 1);
        x
    });
    let outer = 7;
    assert_eq!(*identity(&outer), 7);
    {
        let inner = 9;
        assert_eq!(*identity(&inner), 9);
    }
    assert_eq!(calls.get(), 2);
}

The bound relates each returned reference to that call's input. It does not require the captured calls value to be 'static. The closure borrows the counter and cannot outlive it. Cell permits updating the counter through a shared reference, so this closure still implements Fn.

Put the bound where the closure is created, as in linked(|x| ...). A closure already inferred in another context may not satisfy a more general bound later. Adding a helper after that point is not a universal repair.

Context matters more than whether there is a binding

A stored closure can return a reference successfully. For example, an unannotated let identity = |x| x; can infer an argument type from a local call. That does not prove it works for every reference lifetime required by a higher-ranked API. Conversely, directly calling an annotated closure can still produce the lifetime error; avoiding a binding is not a general fix.

Iterator adapters provide another useful context. In this example, each closure input is a reference to a particular owned String, and the result borrows from that same string:

fn main() {
    let names = [String::from("ada"), String::from("bo")];
    let borrowed: Vec<&str> = names.iter().map(|name| name.as_str()).collect();
    assert_eq!(borrowed, ["ada", "bo"]);
}

Keep the owner alive while the collected references are used. Creating a temporary vector, collecting references into it and using those references after the statement ends fails for a separate reason: the temporary owner has already been dropped. A callable signature cannot extend its lifetime.

Choosing a repair

First identify what the returned reference borrows from. If it borrows from an argument, describe that relationship in a function signature, function pointer type or callable bound. If it borrows from captured data, the capture's lifetime also matters. If the owner is already gone, fix the ownership scope.

Type inference uses expected types, annotations and uses throughout the surrounding context. It is not simply a rule that the first call determines everything. Still, an ordinary identity closure does not become generic over unrelated types: one closure used with both an integer and a String gives a type mismatch. Use a generic function when that is the API you need.

step 19 of 22

Ownership III: Lifetimes, Explicitly

Up next Temporary scopes: E0716 and what edition 2024 changed