The signature that refuses to compile
You write a helper function that accepts a closure. The closure takes a reference. You pass it in. The compiler rejects you with a wall of text about higher-ranked types and lifetime subtyping. The code looks perfectly fine. The reference exists long enough. The closure matches the signature. Yet Rust refuses to compile it. This happens when the compiler cannot prove that a lifetime relationship holds for every possible substitution. The error points to a concept called subtype lifetimes.
How lifetime subtyping actually works
Lifetime subtyping is the compiler's way of comparing how long references live. If lifetime 'a outlives lifetime 'b, then 'a is a subtype of 'b. You can safely use a 'a reference anywhere a 'b reference is expected. The longer-lived reference covers the shorter window. Think of it like a time window on a security camera. A recording that spans ten hours covers any five-hour segment inside it. You can hand the ten-hour clip to someone asking for a five-hour window. You cannot hand a five-hour clip to someone demanding ten hours.
The borrow checker uses this relationship constantly. It verifies that you never hand out a reference that expires before the caller needs it. When you introduce generics or closures, the compiler loses concrete lifetime names. It only sees placeholders. It must prove the outlives relationship holds for every possible lifetime that could fill those placeholders. That universal quantification is what triggers higher-ranked subtype checks.
Trust the outlives relationship. It is the mathematical backbone of Rust's memory safety.
The minimal case
/// Applies a closure to a string slice.
fn apply_closure<F>(f: F, input: &str)
where
F: Fn(&str),
{
// The closure receives a reference tied to the input parameter.
f(input);
}
This compiles. Rust elides the lifetime in Fn(&str) and automatically expands it to for<'a> Fn(&'a str). The for<'a> syntax tells the compiler: this closure must work with any reference lifetime 'a. The compiler checks the subtype relationship for every possible 'a and approves it.
Break the pattern and the compiler stops cooperating.
/// Tries to store a closure that expects a specific lifetime.
fn store_closure<F>(f: F)
where
F: Fn(&str) + 'static,
{
// We never call it, we just store it in a static collection.
let _ = f;
}
If you try to pass a closure that captures a reference with a limited lifetime, the compiler rejects it. The closure signature promises to work with any 'a, but the captured reference only lives for the current scope. The subtype check fails because a short-lived reference is not a subtype of a long-lived one.
Keep your closure signatures aligned with the actual lifetime of captured data. Mismatched promises trigger immediate rejections.
What the compiler checks under the hood
When the compiler encounters a higher-ranked bound, it performs a substitution test. It picks an arbitrary lifetime variable, plugs it into the signature, and checks the outlives constraints. If the constraint holds, it moves to the next substitution. It repeats this process symbolically. The compiler does not test actual values. It tests the type relationships.
The subtype check verifies two directions. First, it ensures the argument lifetime is long enough for the parameter. Second, it ensures the return lifetime does not outlive the input. When a closure signature uses elided lifetimes, Rust expands them to higher-ranked bounds. The compiler then verifies that the provided closure satisfies the universal quantifier. If the closure captures a reference, that reference's lifetime becomes part of the closure's type. The compiler compares the captured lifetime against the required 'a. If the captured lifetime is shorter, the subtype relationship breaks. The check fails.
This is why for<'a> appears in error messages. The compiler is telling you it cannot prove the relationship for all 'a. It found a substitution where the outlives constraint is violated.
Variance plays a supporting role here. Function parameters are contravariant, meaning they accept longer lifetimes than specified. Return values are covariant, meaning they produce shorter lifetimes. The compiler uses these variance rules to widen or narrow the acceptable lifetime ranges during the subtype check. When variance and higher-ranked bounds collide, the compiler must prove the relationship holds across the entire variance spectrum. That proof is what the HigherRankedSubtypeError represents when it fails.
Read the variance rules as boundaries, not suggestions. They dictate exactly which lifetime substitutions the compiler will accept.
A realistic middleware scenario
Consider a logging middleware that processes request headers. The middleware receives a closure to extract a value from the headers. The headers borrow from the request, but the request lifetime is abstracted away by the framework.
/// Extracts a value using a user-provided closure.
fn extract_header<F, R>(headers: &[(&str, &str)], extractor: F) -> Option<R>
where
// The closure must accept any slice reference, regardless of scope.
F: for<'a> Fn(&'a [(&str, &str)]) -> Option<R>,
{
// We hand the slice to the closure with its actual lifetime.
extractor(headers)
}
The for<'a> bound is explicit here. The framework hands the closure a reference with an unknown lifetime 'a. The closure must accept any slice reference, regardless of how long the underlying data lives. If you omit for<'a> and write Fn(&[(&str, &str)]), Rust still expands it to a higher-ranked bound, but the error messages become harder to read when something goes wrong. Making it explicit clarifies the contract.
Now look at a closure that tries to hold onto the reference.
/// Attempts to return a reference from inside the closure.
fn bad_extractor(headers: &[(&str, &str)]) -> Option<&str> {
// The returned reference is tied to the input slice lifetime.
headers.first().map(|(_, v)| v)
}
If you pass this to extract_header, the compiler rejects it. The closure returns a reference tied to the input slice lifetime. The for<'a> bound requires the closure to work with any 'a, but the return type leaks 'a outside the closure call. The compiler cannot guarantee the returned reference stays valid. The subtype check fails because the output lifetime is not bounded by the input lifetime in a way that satisfies the universal quantifier.
Make your closure return types owned when the input lifetime is abstracted. Leaking references across higher-ranked boundaries is a fast track to borrow checker rejections.
Where the checks fail
The most common failure is mismatched elision. You write a function signature that looks correct, but the compiler infers a concrete lifetime instead of a higher-ranked one. You get E0308 (mismatched types) with a note about expected for<'a> Fn(&'a T) but found Fn(&'0 T). The '0 is an anonymous lifetime the compiler invented. It means the closure is tied to a specific scope, not a universal one.
Another trap is trait objects. You cannot put for<'a> bounds directly on a trait object like &dyn Fn(&str). The compiler needs a concrete type to perform the substitution check. You get E0277 (trait bound not satisfied) when the compiler cannot prove the object implements the higher-ranked trait. The fix is usually to use generics instead of trait objects, or to wrap the closure in a struct that implements the trait.
Convention aside: the Rust community writes for<'a> explicitly only when the default elision causes confusion or when the bound spans multiple parameters. In most cases, Fn(&str) is enough. The compiler handles the expansion silently. You only need the explicit syntax when the error message points to a higher-ranked subtype mismatch.
Treat the error message as a map. Follow the lifetime names back to their source, and adjust the bound to match the actual data flow.
Choosing the right bound
Use for<'a> when you need a closure or function pointer to accept references of any lifetime, especially when the lifetime is abstracted by a generic or trait. Use concrete lifetime parameters like fn<'a, F>(f: F) where F: Fn(&'a T) when the caller and callee share a specific, known lifetime scope. Use elided lifetimes like Fn(&T) for simple, single-scope operations where the compiler can safely infer the universal quantifier. Use trait objects like &dyn Fn(&str) only when you need dynamic dispatch and can guarantee the closure does not capture short-lived references.
Pick the bound that matches your data flow, not the one that looks shorter. The borrow checker rewards precision.