Fix E0067: illegal erase coercion

rust intermediate rustc 1.0+

Fix E0067: illegal erase coercion

Error E0067 is a Rust compiler error that signals an attempt to perform an illegal “erase” coercion. In Rust’s type system, certain type conversions are allowed implicitly (coercions), but others are not. This error occurs when you try to erase a lifetime or otherwise remove type information in a way that Rust’s coercion system does not permit.

1. Symptoms

When E0067 occurs, you will see output similar to this:

error[E0067]: illegal erase coercion –> src/main.rs:10:12 | 10 | let g: fn() = f; | ^^^^ | = note: this error was previously described as “illegal erase coercion”


Or in more complex scenarios:

error[E0067]: illegal erase coercion –> src/lib.rs:15:9 | 15 | callback(f) | ^^^^^^^ illegal erase coercion | = note: expected fn(&'a i32) | found fn(i32)


Common symptoms that precede this error:

- Function pointers with lifetime parameters being assigned to types without them
- Generic functions accepting `fn()` when the caller provides `fn<'a>()`
- Attempting to coerce higher-ranked trait bounds (HRTBs) incorrectly
- Type mismatch between function signatures with and without lifetime annotations

## 2. Root Cause

Rust's coercion system allows certain implicit conversions, but lifetime erasure is generally **not** one of them. When you have a function pointer like `fn(&'a i32)`, you cannot implicitly coerce it to `fn(i32)` because the lifetime carries semantic meaning that would be lost in the erasure.

The "erase" in error message refers to the act of removing type parameters or lifetime annotations. Rust only permits "合法的" (legal) coercions:

| From | To | Legal? |
|------|----|--------|
| `&T` | `&T` where `T: Trait` | ✅ Via unsizing |
| `fn(&'a T)` | `fn(&'a T)` (same signature) | ✅ Identity coercion |
| `fn(&'a T)` | `fn(T)` | ❌ **Illegal erase** |
| `&'a T` | `&'static T` | ✅ Only if `'a: 'static` |
| `&'a mut T` | `&'a T` | ✅ Unsafe coercion |

**E0067** specifically occurs when:

1. **Attempting to remove lifetime parameters from function types** - Going from `fn(&'a i32)` to `fn(i32)` erases the lifetime parameter.
2. **Type parameter erasure in generic contexts** - Trying to coerce a generic type to a concrete type that loses type information.
3. **Improper HRTB handling** - Higher-ranked trait bounds have specific rules that, when violated, can trigger this error.

```rust
// Root cause example 1: Lifetime erasure on function pointers
fn takes_fn_with_lifetime<'a>(f: fn(&'a i32) -> &'a i32) {
    // We cannot do this - it's an illegal erase coercion:
    let g: fn(i32) -> i32 = f; // E0067
}

// Root cause example 2: Trait object coercion issues
trait MyTrait {
    fn call(&self);
}

fn needs_static_trait_object(_: &dyn MyTrait) {}

fn works_with_borrowed<'a>(t: &'a dyn MyTrait) {
    // This might trigger E0067 if lifetime matters
    needs_static_trait_object(t); // Potential E0067 if lifetime constraints violated
}

// Root cause example 3: Generic parameter to concrete type
fn coerce_generic<'a, T: Fn()>(f: T) {
    let _fn: fn() = f; // E0067 if T carries lifetime information
}

3. Step-by-Step Fix

Fix 1: Avoid Lifetime Erasure by Using Matching Types

The safest approach is to ensure your types match exactly without relying on implicit erasure.

Before:

fn process<'a>(callback: fn(&'a i32) -> &'a i32) {
    // This creates a conflict when trying to use a static function
    let static_fn: fn(i32) -> i32 = callback; // E0067
}

fn main() {
    fn static_callback(x: &i32) -> &i32 { x }
    process(static_callback);
}

After:

fn process<'a>(callback: fn(&'a i32) -> &'a i32) {
    // Keep the function type with lifetime, or change the signature
    let _ = callback;
}

fn main() {
    fn static_callback<'a>(x: &'a i32) -> &'a i32 { x }
    process(static_callback);
}

Fix 2: Adjust Function Signatures to Accept Static Lifetimes

If you need to pass static function pointers, adjust the function signature:

Before:

fn register_callback<F>(f: F)
where
    F: Fn(),
{
    // Cannot coerce F to fn()
}

fn main() {
    register_callback(|| println!("hello"));
}

After:

fn register_callback(f: fn()) {
    f();
}

fn main() {
    fn closure_wrapper() {
        println!("hello");
    }
    register_callback(closure_wrapper);
}

Fix 3: Use Type Annotations Explicitly

When working with function pointers, be explicit about the types:

Before:

fn call_fn_ptr(f: fn(i32), x: i32) -> i32 {
    f(x)
}

fn main() {
    let captured = 5;
    let f = || captured; // Closure type: impl Fn() -> i32
    
    // Cannot coerce closure to fn()
    call_fn_ptr(f, 10); // E0067
}

After:

fn call_fn_ptr<F>(f: F, x: i32) -> i32
where
    F: Fn() -> i32,
{
    f()
}

fn main() {
    let captured = 5;
    let f = || captured;
    
    // Use generic or convert closure to proper form
    call_fn_ptr(f, 10); // Works with generic
}

Fix 4: Erase Lifetimes Explicitly with Workaround Functions

Create wrapper functions that explicitly handle lifetime conversion:

Before:

fn accept_fn_ptr(f: fn(&'static str)) {
    f("hello");
}

fn lifetime_fn(s: &str) {
    println!("{}", s);
}

fn main() {
    accept_fn_ptr(lifetime_fn); // E0067 if lifetimes don't match exactly
}

After:

fn accept_fn_ptr(f: fn(&'static str)) {
    f("hello");
}

fn main() {
    // Create a function that accepts 'static
    fn static_wrapper(s: &str) {
        println!("{}", s);
    }
    accept_fn_ptr(static_wrapper); // Works
}

Fix 5: Restructure Generics to Avoid Coercion Conflicts

Before:

trait Processor {
    fn process(&self, input: &str) -> String;
}

fn process_all<'a, P: Processor>(items: &[&str], processor: P)
where
    P: 'a,
{
    for item in items {
        // Problem: processor might not live 'a
        let result = processor.process(item);
        println!("{}", result);
    }
}

After:

trait Processor {
    fn process(&self, input: &str) -> String;
}

fn process_all<P: Processor + Clone>(items: &[&str], processor: P) {
    // Clone processor for each iteration to avoid lifetime issues
    for item in items {
        let result = processor.clone().process(item);
        println!("{}", result);
    }
}

4. Verification

After applying fixes, verify the error is resolved:

cargo build 2>&1 | grep -E "(error|warning: E0067)"

A successful build will show no E0067 errors:

$ cargo build
   Compiling my_crate v0.1.0 (/path/to/my_crate)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s

For more comprehensive verification, run the test suite:

cargo test 2>&1

If the error persists, double-check your function signatures:

// Verify type compatibility
fn main() {
    // This should compile without E0067
    type FnWithLifetime = fn(x: &i32) -> &i32;
    
    fn my_fn<'a>(x: &'a i32) -> &'a i32 { x }
    
    let _: FnWithLifetime = my_fn; // Should work if lifetimes compatible
    
    println!("Type coercion successful");
}

5. Common Pitfalls

Pitfall 1: Confusing fn with closures

Closures are not the same as function pointers. You cannot coerce impl Fn() to fn() directly:

// This will NOT work
let closure = || println!("hello");
let fn_ptr: fn() = closure; // E0067 or E0308

Pitfall 2: Assuming 'static lifetime is always inferred

Even with &'static str, you must explicitly ensure the function signature matches:

// Be explicit about lifetimes
fn with_static_param(f: fn(val: &'static str)) {
    f("static string");
}

fn main() {
    fn normal_fn(s: &str) { // This is NOT fn(&'static str)
        println!("{}", s);
    }
    with_static_param(normal_fn); // May cause issues
}

Pitfall 3: Forgetting that trait objects carry lifetimes

Trait objects like &dyn Trait have a lifetime. You cannot always coerce them freely:

trait Printable {
    fn print(&self);
}

fn print_any(p: &dyn Printable) {
    p.print();
}

fn print_static(p: &'static dyn Printable) {
    p.print();
}

fn main() {
    let value = String::from("hello");
    let printable = PrintableStruct { data: value };
    
    // Lifetime conflict - printable is not 'static
    print_static(&printable); // Potential E0067
}

Pitfall 4: Attempting coercion in type aliases

Type aliases do not change coercion rules:

type Callback = fn(x: &i32);

fn register(cb: Callback) {}

fn main() {
    fn with_lifetime<'a>(x: &'a i32) -> &'a i32 { x }
    
    // Error: lifetime mismatch
    register(with_lifetime); // E0067
}

Pitfall 5: Generic bounds that inadvertently require erasure

// This compiles fine
fn good<T: Clone>(t: T) -> T {
    t.clone()
}

// This might cause issues with function pointers
fn problematic<F: Fn()>(f: F) -> F {
    f
}

E0308: Mismatched Types Often accompanies E0067 when the type mismatch is detected. The “illegal erase coercion” is a specific case of type mismatch.

error[E0308]: mismatched types
  --> src/main.rs:5:20
   |
5  |     let f: fn(i32) = |x| x; // E0308 as well as potentially E0067
   |                 --- ^ expected fn pointer, found closure

E0597: Lifetime must be valid Related to lifetime errors that can occur alongside coercion issues.

E0605: Invalid assignment to self May appear when self-referential types or lifetime issues are involved.

E0277: Trait bound not satisfied Frequently seen when generics with lifetime constraints fail to satisfy trait bounds due to coercion problems.

Key differences:

  • E0067 specifically concerns “erase” operations (lifetime or type parameter removal)
  • E0308 is a general type mismatch
  • E0597 deals with lifetime validation
  • E0277 is about trait bounds

Understanding that E0067 is about illegal coercion (removing type information) helps distinguish it from general type errors. The fix typically involves either adjusting types to match exactly, removing the need for coercion, or restructuring the code to avoid the problematic pattern.

// Comprehensive example combining related concepts
fn main() {
    // E0067 prevention pattern
    fn create_processor() -> impl Fn(i32) -> i32 {
        move |x| x * 2
    }
    
    let processor = create_processor();
    let result = processor(5);
    println!("Result: {}", result);
}

This pattern uses impl Fn instead of fn to avoid the coercion issue entirely, which is often the cleanest solution when working with closures that capture environment.