When the type name gets in the way
You're building a function that returns a closure. The closure captures some local state, so it has a unique type generated by the compiler. You try to return it, and the compiler rejects you because you can't name the type. You switch to Box<dyn Fn()> to make it compile. Now you're paying for a heap allocation and dynamic dispatch on every call. You want the flexibility of a trait but the performance of a concrete type. You reach for impl Fn(). The compiler accepts it. The allocation disappears. The dispatch becomes direct. You just used an existential type.
Existential types let you hide the concrete type behind a trait bound while keeping the performance of a known type. They are the zero-cost way to return "something that implements this trait" without forcing the caller to know what that something is.
What existential means
The name comes from logic. An existential type means "there exists a type that satisfies this condition." In Rust, you write impl Trait in type position to express this. The function or struct declares that it holds a value of some type that implements the trait, but it refuses to tell you which type.
Think of a sealed box labeled impl Display. The factory packing the box knows exactly what's inside. It might be an integer, a string, or a custom struct. The factory seals the box and hands it to you. You cannot see inside. You cannot change the contents. You can only call methods defined on Display. The crucial rule is that the box contains exactly one concrete type. Every time you call the function, the box might contain a different type, but for that specific call, the type is fixed and known to the compiler.
This is different from a trait object. A trait object (dyn Trait) is like a bag labeled "Implements Display" where you can put different items in the same bag. An existential type is a sealed box. One box, one type. The caller never learns the type, but the compiler always knows it.
Minimal example
The most common use is returning a closure. Closures have unique, unnameable types. impl Trait lets you return them without boxing.
/// Returns a closure that doubles its input.
/// The concrete type is a unique closure type, hidden by impl Fn.
fn make_doubler() -> impl Fn(i32) -> i32 {
// The compiler infers a concrete closure type here.
// impl Fn hides the type name but preserves the concrete value.
|x| x * 2
}
fn main() {
let doubler = make_doubler();
// You can call the closure. You don't know the type.
// The compiler knows the type and generates a direct call.
println!("{}", doubler(5));
}
The type is hidden from you, not from the compiler.
How the compiler handles it
When you write impl Trait in a return type, the compiler looks at the function body. It finds the concrete type being returned. It checks that this type implements the trait. It records the mapping between the function and the concrete type. When code calls the function, the compiler generates code that uses the concrete type directly.
There is no vtable. There is no indirection. The generated code is identical to what you would get if you had named the type explicitly. The optimization level does not change. The size of the return value is known at compile time. You get the abstraction of a trait with the performance of a concrete type.
This works because the type is fixed per call site. If you call make_doubler ten times, the compiler generates the same concrete type ten times. If you have two functions returning impl Fn(), they might return different types. The compiler tracks each one separately. The type is existential relative to the caller, but concrete relative to the compiler.
Trust the size. If you can return it by value, you're not paying for a box.
Size and layout
Because impl Trait resolves to a concrete type, the compiler knows the size. You can return impl Trait by value. You can put it on the stack. The return value has a fixed size determined by the concrete type.
This is a major difference from dyn Trait. A trait object is unsized. The compiler does not know how big it is. You cannot return dyn Trait by value. You must box it or borrow it. This adds indirection and often requires heap allocation.
/// Returns a value by value. No heap allocation.
/// The size is known at compile time.
fn get_value() -> impl std::fmt::Display {
42
}
/// Returns a boxed trait object. Heap allocation required.
/// The size is unknown at compile time.
fn get_dyn() -> Box<dyn std::fmt::Display> {
Box::new(42)
}
The first function returns an integer directly. The second function allocates memory on the heap, stores the integer there, and returns a pointer. impl Trait avoids this overhead entirely. Use it when you want to hide the type but keep the value on the stack.
Struct fields and opaque types
Rust 1.65 added support for impl Trait in struct fields. This lets you hide the type of a field inside the struct definition. The struct itself does not know the type. The function that creates the struct does.
/// A logger that uses a hidden formatter.
/// The formatter type is opaque to users of the struct.
struct Logger {
// The field holds a concrete type, but the struct definition doesn't name it.
// This is an opaque type. Callers cannot access the concrete type.
formatter: impl Fn(&str) -> String,
}
/// Creates a logger with a specific formatter.
/// The concrete type is chosen here, not by the caller.
fn create_logger() -> Logger {
Logger {
formatter: |msg| format!("[LOG] {}", msg),
}
}
The community calls this an "opaque type." It's a way to encapsulate the implementation of a field. If you change the closure inside create_logger, you don't break callers of Logger, as long as the new type still implements the trait. The struct definition stays stable. The implementation can evolve without API changes.
Convention aside: when you use impl Trait in struct fields, you're building an opaque type. This is the Rust way to hide implementation details without generics. It's cleaner than Box<dyn Trait> and more flexible than generics.
Pitfalls and errors
Existential types have strict rules. The compiler enforces them aggressively.
You cannot return different types based on a condition. impl Trait is a promise of a single type. If you have an if branch returning i32 and an else returning String, the compiler rejects you with E0308 (mismatched types). The compiler needs to pick one concrete type for the whole function.
/// This fails. The compiler cannot pick one type.
fn bad_return(flag: bool) -> impl std::fmt::Display {
if flag {
42
} else {
// E0308: mismatched types.
// The compiler sees i32 and &str. They are different types.
// impl Trait requires a single concrete type per function.
"hello"
}
}
You also cannot return references to local variables. The existential type must own its data or have a lifetime that outlives the function. If you try to return &s where s is local, you get a lifetime error. The type inside the box must be valid after the function returns.
/// This fails. The reference dangles.
fn bad_lifetime() -> impl std::fmt::Display {
let s = String::from("hello");
// E0515: cannot return reference to local variable.
// The existential type would contain a reference to s.
// s is dropped when the function returns.
&s
}
If the concrete type does not implement the trait, you get E0277 (trait bound not satisfied). The compiler checks the implementation at the definition site. If you change the return type to something that doesn't implement the trait, the error appears immediately.
/// This fails. i32 does not implement Clone? No, it does.
/// Let's use a type that doesn't implement Display.
struct NoDisplay;
fn bad_trait() -> impl std::fmt::Display {
// E0277: NoDisplay does not implement Display.
// The compiler checks the trait bound against the concrete type.
NoDisplay
}
The type must satisfy the trait. The compiler verifies this once, at the function definition. Callers don't need to check anything. They just get the trait behavior.
The argument trap
impl Trait has two faces. In type position, it's existential. In argument position, it's universal. This distinction trips up many developers.
When you write fn foo(x: impl Display), you are not using an existential type. You are using a shorthand for generics. It means "for all types T where T implements Display." The caller chooses the type. The function adapts to the caller.
/// This is universal quantification.
/// The caller chooses the type.
/// Equivalent to fn print<T: Display>(x: T).
fn print(x: impl std::fmt::Display) {
println!("{}", x);
}
/// This is existential quantification.
/// The function chooses the type.
/// The caller cannot choose.
fn get_value() -> impl std::fmt::Display {
42
}
Ask yourself: is impl Trait a type or an argument? If it's a type, the function chooses the type. If it's an argument, the caller chooses the type. Existential types are only in type position. Argument position is always universal.
Decision matrix
Use impl Trait in return position when the function produces a single concrete type that you want to hide. The caller gets the trait behavior without the type name.
Use impl Trait in struct fields when you want to encapsulate a concrete implementation inside a struct without exposing the type in the struct definition. This lets you change the implementation without breaking the struct's API.
Use dyn Trait when you need a collection of heterogeneous types. A Vec<dyn Trait> can hold different types. A Vec<impl Trait> cannot, because impl Trait collapses to a single type per use.
Use generics when the caller needs to construct the type or pass it to other functions that require the concrete type. Generics push the type choice to the caller. impl Trait keeps the choice inside the function.
Use impl Trait for zero-cost abstraction. If you can return the value by value, prefer impl Trait over Box<dyn Trait>. Avoid heap allocation when the type is known at compile time.