What is Higher-Ranked Trait Bounds HRTB

Higher-Ranked Trait Bounds (HRTB) ensure a generic type implements a trait for all possible lifetimes, enabling flexible closure and function pointer usage.

The closure that breaks when you don't write lifetimes

You are writing a utility function. It takes a slice of strings and a closure. The function iterates over the slice and calls the closure with each element. You write the signature, pass a closure, and the compiler screams. You didn't write a single lifetime annotation. The error points to the closure argument and complains about mismatched lifetimes.

This happens when a generic function needs to accept a closure that borrows data, but the function creates the borrowed data internally. The function doesn't know how long the data lives. It needs a closure that can handle a reference no matter how long that reference lives. Standard trait bounds tie the closure to one specific lifetime. Higher-Ranked Trait Bounds (HRTB) let you demand a closure that works for any lifetime.

What "higher-ranked" actually means

The term comes from logic. A "rank" refers to how many quantifiers wrap a statement. In Rust, a lifetime parameter like 'a is a variable. A standard bound says "there exists a lifetime 'a such that this trait holds." A higher-ranked bound says "for all lifetimes 'a, this trait holds."

Think of a key. A standard bound is a key cut for a specific lock. It works for that lock and that lock only. A higher-ranked bound is a master key. It works for any lock you throw at it. The function holding the master key can pick a lock, hand the key to the closure, and the closure will turn it. The function doesn't care which lock it is. The closure promised it handles all of them.

In code, the syntax is for<'a>. The for keyword is the universal quantifier. It binds the lifetime 'a and asserts that the bound holds for every possible choice of 'a.

// Standard bound: F works for some specific lifetime 'a.
// The compiler picks 'a, and F must match it.
fn call_once<F>(f: F) 
where 
    F: Fn(&i32), 
{
    // This actually compiles because Fn(&i32) is sugar for HRTB.
    // See the convention aside below.
    let x = 5;
    f(&x);
}

The sugar trap and the real HRTB

Rust hides HRTB behind syntax sugar. When you write Fn(&i32), the compiler desugars it to for<'a> Fn(&'a i32). This is why simple cases compile without you typing for<'a>. The sugar covers the most common pattern: a closure that accepts a reference and does something with it.

The sugar breaks down when lifetimes interact in complex ways. You need explicit HRTB when the closure returns a reference that depends on the input lifetime, or when you have multiple lifetime parameters that must be quantified independently.

Consider a function that transforms a reference and returns a reference. The output lifetime must be tied to the input lifetime. If you use a standard bound, the compiler cannot express that dependency.

// This fails. The compiler cannot infer how the output lifetime relates to the input.
// It sees Fn(&str) -> &str and gets confused about which lifetime governs the return.
fn transform<F>(f: F) 
where 
    F: Fn(&str) -> &str, 
{
    let s = String::from("hello");
    let result = f(&s);
    // result borrows from s. The bound must promise the closure respects that.
}

The compiler rejects this with E0495 (cannot infer an appropriate lifetime due to conflicting requirements). The closure signature Fn(&str) -> &str is ambiguous. Does the return value live as long as the input? Or does it live for some other lifetime? You need to quantify the lifetime and link the two.

fn transform_hrtb<F>(f: F) 
where 
    for<'a> F: Fn(&'a str) -> &'a str, 
{
    let s = String::from("hello");
    // The closure accepts a &'a str and returns a &'a str.
    // The function picks 'a to be the lifetime of s.
    // The closure's promise guarantees this is safe.
    let result = f(&s);
}

The for<'a> quantifier binds 'a inside the trait bound. It says "F implements Fn for any lifetime 'a, and when it does, the input and output share that same 'a." The function can now pick the lifetime of s and pass it to f. The closure's implementation must prove it can handle any 'a and return a reference with that same 'a.

Convention aside: for<'a> is rarely typed in application code. It appears in library definitions, trait bounds, and complex generic helpers. When you see it in error messages, it usually means the compiler tried to desugar a bound and failed, or you are dealing with a trait object that requires explicit quantification.

Walkthrough: who picks the lifetime?

Lifetime quantification shifts responsibility. In a standard bound, the function signature introduces a lifetime parameter, and the caller must satisfy it. In an HRTB, the bound quantifies over the lifetime, and the callee gets to pick it.

Look at the types.

// Standard bound with explicit lifetime.
// The caller provides data with lifetime 'a.
// The closure must work for that specific 'a.
fn caller_provides<'a, F>(data: &'a str, f: F) 
where 
    F: Fn(&'a str), 
{
    f(data);
}

// HRTB bound.
// The function creates data internally.
// The function picks the lifetime of the data.
// The closure must work for whatever lifetime the function picks.
fn callee_provides<F>(f: F) 
where 
    for<'a> F: Fn(&'a str), 
{
    let local = String::from("data");
    f(&local); // Function picks 'a = lifetime of local.
}

In caller_provides, the lifetime 'a is a parameter of the function. The caller decides 'a by passing a reference. The closure F must implement Fn for that specific 'a. If the caller passes a reference to a local variable, 'a is short. If the caller passes a static string, 'a is 'static. The closure must match.

In callee_provides, there is no lifetime parameter on the function. The bound for<'a> F: Fn(&'a str) quantifies 'a inside the bound. The function creates local and calls f. The function effectively says "I have a reference with some lifetime. Your closure promised to work for any lifetime. Therefore, it works for this lifetime." The burden of proof is on the closure. The closure must be generic over the lifetime of its argument.

This distinction matters when you return closures. If a function returns a closure that captures a reference, the closure's lifetime is tied to the captured data. HRTB allows you to express that the returned closure can accept references of any lifetime, independent of what it captures.

Realistic example: a query builder

Query builders often use HRTB. They accept closures that define conditions. The closure borrows data from the caller, but the query builder executes the query later. The builder needs a closure that can handle references to the caller's data, regardless of how long that data lives.

struct QueryBuilder {
    conditions: Vec<Box<dyn Fn(&str) -> bool>>,
}

impl QueryBuilder {
    // We need HRTB here because the closure is stored.
    // The closure must work for any lifetime of the string slice it receives.
    fn filter<F>(&mut self, f: F) 
    where 
        for<'a> F: Fn(&'a str) -> bool + 'static, 
    {
        // We box the closure to store it.
        // The bound ensures the closure can handle any &'a str.
        self.conditions.push(Box::new(f));
    }

    fn execute(&self, rows: &[String]) -> Vec<String> {
        rows.iter()
            .filter(|row| {
                // We call each condition with a reference to the row.
                // The row lives only for this iteration.
                // The condition must handle this short lifetime.
                self.conditions.iter().all(|cond| cond(row.as_str()))
            })
            .cloned()
            .collect()
    }
}

fn main() {
    let mut qb = QueryBuilder { conditions: Vec::new() };
    
    let prefix = "admin";
    // The closure captures `prefix`.
    // It accepts a &str and returns bool.
    // HRTB allows this closure to be stored and called with short-lived references.
    qb.filter(|row: &str| row.starts_with(prefix));
    
    let rows = vec![String::from("admin_user"), String::from("guest")];
    let result = qb.execute(&rows);
    println!("{:?}", result);
}

The filter method stores the closure. The closure might capture variables from the caller. Those captures have their own lifetimes. The closure also accepts an argument &'a str. The argument lifetime is independent of the captures. HRTB separates these concerns. The for<'a> quantifies the argument lifetime. The + 'static bound ensures the closure itself (including captures) lives long enough to be stored.

Without HRTB, you couldn't express that the closure accepts a reference of arbitrary lifetime while being stored. You would be forced to require 'static references everywhere, which kills ergonomics. HRTB lets the closure be flexible about its input while remaining safe about its storage.

Pitfalls and compiler errors

HRTB introduces subtle traps. The most common is confusing HRTB with 'static. HRTB does not mean the data lives forever. It means the closure can handle data that lives for any duration, including very short durations.

If you try to return a reference from a closure that doesn't match the input lifetime, the compiler rejects it.

fn bad_transform<F>(f: F) 
where 
    for<'a> F: Fn(&'a str) -> &'a str, 
{
    let s = String::from("hello");
    let result = f(&s);
    // result has lifetime of s.
    // If f returns a reference to something else, the bound is violated.
}

// This closure violates the bound.
// It ignores the input and returns a static reference.
// The bound requires the output lifetime to match the input lifetime.
let closure = |_input: &str| -> &str { "static" };
// bad_transform(closure); // Error: mismatched types.

The compiler rejects this with E0308 (mismatched types) or a lifetime mismatch error. The bound for<'a> Fn(&'a str) -> &'a str forces the output to share the input's lifetime. A closure returning a static string has output lifetime 'static. 'static is not the same as 'a for all 'a. The closure fails the universal quantifier.

Another pitfall is assuming HRTB solves all lifetime issues. HRTB only quantifies lifetimes in trait bounds. It cannot fix code where you try to move a reference out of a closure or violate borrowing rules inside the closure. The closure body must still obey Rust's ownership rules.

Convention aside: When writing HRTB bounds, keep the quantified lifetime name consistent. Use 'a or 'b. Don't use 'input and 'output unless the distinction is critical for readability. The community convention is single-letter names for quantified lifetimes in bounds, reserving descriptive names for function parameters.

When to use HRTB

Use for<'a> when you need a closure to accept a reference and the function creates the reference locally, so the function must pick the lifetime. Use for<'a> when the closure returns a reference that depends on the input lifetime, like for<'a> Fn(&'a T) -> &'a U. Use for<'a> when defining a trait that accepts closures and you want the trait to be usable with closures that borrow data of arbitrary lifetimes.

Reach for Fn(&T) sugar when the closure just consumes a reference and returns a concrete type or nothing. The sugar desugars to HRTB automatically, so you get the flexibility without the syntax.

Pick explicit lifetime parameters like fn foo<'a, F: Fn(&'a T)> when the caller provides the data and the function must respect that specific lifetime. The caller controls 'a, and the closure must match it. HRTB is for when the callee controls the lifetime or when the lifetime is quantified over the bound.

Avoid HRTB when you can express the logic with owned types. Cloning data or using Cow sometimes removes the need for complex lifetime bounds. HRTB is a tool for borrowing, not a replacement for good data modeling.

HRTB is the bridge between generic flexibility and lifetime safety. Cross it when the compiler demands a promise you can only make with a quantifier.

Where to go next