How to Use Higher-Ranked Trait Bounds (HRTBs) in Rust

Use the `for<'a>` syntax before a trait bound to require a type to implement that trait for all possible lifetimes.

When a closure needs to handle any lifetime

You write a utility function to transform text. It takes a closure so the caller can decide the logic. The closure needs a &str. You write fn transform<F>(f: F) where F: Fn(&str). It works for simple cases. Then you try to pass a closure that captures a variable from the calling scope. The compiler rejects you. You add a lifetime parameter to transform. Now transform is locked to one specific lifetime. You can't use it with a string literal, a local variable, and a heap-allocated string in the same program. The type system is fighting you because it thinks the closure is committed to one lifetime, but you need it to work with any lifetime.

Higher-Ranked Trait Bounds (HRTBs) solve this. They let you require a type to implement a trait for all possible lifetimes, not just one. The syntax is for<'a>. It tells the compiler: "Don't pick a lifetime for this bound. Instead, require that the type implements the trait for every possible lifetime independently."

The universal quantifier for lifetimes

Think of a normal lifetime bound as a contract for a specific scenario. Fn(&'a str) says "This function works with references of lifetime 'a." The caller picks 'a. The function must handle that one choice.

An HRTB is a universal contract. for<'a> Fn(&'a str) says "This function works with references of any lifetime." The function itself doesn't care about the lifetime. It promises to accept a reference whether it lives for a millisecond or the entire program duration. The caller picks the lifetime for each call, and the function must handle it.

The for keyword acts as a quantifier. It scopes the lifetime variable to the bound. The lifetime 'a inside for<'a> is fresh for every check. It has no connection to lifetimes outside the bound. This independence is what allows the function to remain generic over lifetimes.

Write for<'a> when the lifetime belongs to the caller, not the function.

Minimal example

Consider a function that takes a closure and a string. The closure processes the string and returns a reference to a substring. The output reference must live as long as the input string. A plain bound fails here because the compiler cannot link the input lifetime to the output lifetime without an HRTB.

/// Finds a substring using a closure that returns a reference.
fn find_substring<F>(f: F, input: &str) -> &str 
where 
    // HRTB: The closure must work for any lifetime 'a.
    // The output lifetime matches the input lifetime.
    // Without `for<'a>`, the compiler assumes a single fixed lifetime.
    F: for<'a> Fn(&'a str) -> &'a str 
{
    // The closure receives `input` and returns a reference.
    // The return type inherits the lifetime of `input`.
    f(input)
}

fn main() {
    // This closure works for any lifetime.
    // It returns the input as-is.
    let identity = |s: &str| s;
    
    // `find_substring` accepts the closure.
    // The lifetime of the result matches the lifetime of the literal.
    let result = find_substring(identity, "hello world");
    println!("{}", result);
}

The for<'a> introduces a lifetime variable that exists only within the trait bound. The compiler checks that F implements Fn for any 'a the caller might choose. If the caller passes a closure that only works for 'static, the compiler rejects it. The closure must be flexible.

What the compiler checks

When you write for<'a>, the compiler performs a universal check. It asks: "Does this type satisfy the trait for every possible lifetime?" The check happens independently for each lifetime. The type cannot rely on a specific lifetime duration.

Rust has a feature called lifetime elision for trait bounds. If a lifetime appears only in the input arguments of a trait bound, the compiler treats it as higher-ranked automatically. Fn(&str) is sugar for for<'a> Fn(&'a str). You only need to write for<'a> explicitly when the lifetime appears in the output position or when you need to disambiguate. This saves typing but hides the mechanics. Understanding the expansion helps when the compiler complains about complex bounds.

The for keyword shifts the burden. The function promises universality; the closure must deliver.

Realistic usage

HRTBs appear frequently in iterator adapters, callback systems, and API boundaries where closures accept references. A common pattern is a method that takes a closure to extract a key from a reference.

/// A simple cache that stores strings.
struct Cache {
    entries: Vec<String>,
}

impl Cache {
    /// Fetches a value by key, using a closure to resolve the key.
    fn get<F>(&self, f: F) -> Option<&str>
    where
        // The closure takes a reference to the cache and returns a key reference.
        // The key reference must live as long as the cache reference.
        // HRTB ensures the closure works for any borrow of the cache.
        F: for<'a> Fn(&'a Self) -> &'a str,
    {
        // The closure extracts the key from the cache reference.
        // The key lifetime is tied to the cache borrow.
        let key = f(self);
        
        // Search for an entry starting with the key.
        // The returned reference lives as long as the cache borrow.
        self.entries.iter().find(|e| e.starts_with(key)).map(|s| s.as_str())
    }
}

fn main() {
    let mut cache = Cache {
        entries: vec![String::from("apple"), String::from("banana")],
    };
    
    // The closure borrows the cache and returns a reference to the first entry.
    // This satisfies the HRTB because it works for any borrow of the cache.
    let key_closure = |c: &Cache| c.entries.first().map(|s| s.as_str()).unwrap_or("");
    
    // `get` accepts the closure and returns a reference tied to the cache.
    if let Some(value) = cache.get(key_closure) {
        println!("Found: {}", value);
    }
}

Community convention: When you see dyn Fn(&str), assume the HRTB is hidden inside. Trait objects always carry higher-ranked bounds for input lifetimes. dyn Fn(&str) expands to dyn for<'a> Fn(&'a str). You rarely type for<'a> with dyn. You write it with generics. This distinction matters when you choose between static dispatch with generics and dynamic dispatch with trait objects.

Pitfalls and compiler errors

HRTBs fail when the closure cannot satisfy the universal promise. The most common failure is a closure that captures state and returns a reference to that capture.

/// Demonstrates a closure that fails the HRTB check.
fn try_process<F>(f: F) where F: for<'a> Fn(&'a str) -> &'a str {
    let local = String::from("captured");
    
    // This closure returns a reference to `local`.
    // `local` has a fixed lifetime tied to this scope.
    // The closure cannot return a reference of arbitrary lifetime 'a.
    let bad_closure = |_: &str| &local;
    
    // Error: E0597 (borrowed value does not live long enough).
    // The closure violates the universal promise.
    // It only works for the lifetime of `local`, not for all lifetimes.
    f("test");
}

The compiler rejects this with E0597 (borrowed value does not live long enough). The closure promises to work for any lifetime, but the capture has a fixed lifetime. The compiler cannot guarantee that the returned reference will be valid for an arbitrary 'a. The closure is too specific for the universal promise.

Another pitfall is confusing the scope of the lifetime. The 'a in for<'a> is scoped to the bound. It does not leak into the function signature. If you try to use 'a outside the bound, the compiler rejects you with E0261 (use of undeclared lifetime name). The lifetime is a local variable for the trait check.

A closure that captures state is tied to that state's lifetime. It cannot promise to work for all lifetimes. Keep captures out of HRTB closures, or accept that the closure won't compile.

Decision matrix

Use for<'a> when your closure takes a reference and returns a reference, and the output lifetime must match the input lifetime. Use for<'a> when you write a generic helper that must accept closures working with references of any lifetime, such as iterator adapters or callback systems. Use a plain lifetime parameter <'a> when the function signature is already bound to a specific lifetime, like when you return a reference that comes directly from an argument. Use dyn Fn(&str) when you need to store the closure in a struct or return it from a function; trait objects imply the higher-ranked bound automatically. Reach for impl Trait when you want static dispatch and the compiler can infer the HRTB from the input arguments alone.

Match the bound to the lifetime flow. If the lifetime belongs to the caller, use for<'a>. If it belongs to the function, use a parameter.

Where to go next