Where Clauses

A where clause moves generic bounds after the signature so complex constraints remain readable and can apply to types beyond direct parameters.

What it is

Inline bounds such as fn f<T: Display + Clone>(t: T) work well for simple cases. A where clause writes the same constraints after the parameter and return list. This keeps the function’s shape visible when bounds are long. It also supports bounds on types that are not themselves generic parameters, such as T::Item: Clone or String: PartialEq<T>. Where clauses can express lifetime bounds, associated type bounds, and higher-ranked lifetime bounds.

How it works

Write where after the function signature, impl header, trait declaration, type alias, or associated item where allowed. Each line is a predicate: T: Display, I::Item: Copy, 'a: 'b, or for<'a> &'a T: IntoIterator. The predicates become part of the item contract exactly like inline bounds. For GATs, required where clauses may need to be written on the associated type declaration itself, not only on the methods. Where clauses are not only formatting; they unlock constraints that inline syntax cannot place cleanly. They are also the idiomatic home for higher-ranked trait bounds: for<'a> &'a T: IntoIterator. A where predicate on an associated type, such as <I as Iterator>::Item: Clone, constrains the projected type selected by the implementation. Lifetime predicates like 'long: 'short mean the first lifetime outlives the second. Because predicates are part of the item signature, changing them on public items is an API change even if the function body stays the same.

Example

use std::fmt::Display;
 
fn join_display<I>(items: I) -> String
where
    I: IntoIterator,
    I::Item: Display,
{
    let mut out = String::new();
    for item in items {
        if !out.is_empty() {
            out.push_str(", ");
        }
        out.push_str(&item.to_string());
    }
    out
}
 
fn main() {
    assert_eq!(join_display([1, 2, 3]), "1, 2, 3");
}

Common errors

Putting every bound inline can lead to unreadable signatures and less helpful diagnostics, but the compiler errors are the same as with where. A missing associated-item bound often appears as:

error[E0277]: `<I as Iterator>::Item` doesn't implement `std::fmt::Display`

Add I::Item: Display in a where clause instead of adding unrelated bounds to I. For GATs, placing where Self: 'a only on the method may still fail because the required bound belongs to the associated type declaration. Deprecated-looking forms around associated type aliases are avoided by writing the clause after the assigned type in impls: type Ref<'a> = &'a T where Self: 'a;.

Best practice

  • ✅ Use inline bounds for short, obvious constraints and where clauses for multiple or associated-type constraints.
  • ✅ Put one predicate per line when bounds are part of a public API.
  • ✅ Use where to keep the function name, arguments, and return type close together.
  • ✅ Prefer I::Item: Trait in a where clause over contorting the parameter list.
  • ✅ For GATs, place required clauses on the GAT declaration when the trait method signatures require them.

Pitfalls

  • ⚠️ Hiding very broad constraints in a where clause does not make the API simpler; it only moves the complexity.
  • ⚠️ Deprecated forms of where clauses on associated type aliases should be avoided; put the clause after the assigned type.
  • ⚠️ Repeating bounds in many where clauses can signal a missing helper trait or type alias.
  • ⚠️ Overly strong lifetime predicates such as T: 'static can unnecessarily reject borrowed callers.

See also

Generics · Trait Bounds · Associated Types · Generic Associated Types · Lifetimes · Static Dispatch with Generics · Readable Generic APIs · Generics, Traits & Lifetimes

Sources