“Mismatched Types” in Rust

If you’ve written Rust for more than five minutes, you’ve met this error:

error[E0308]: mismatched types

At first glance, it feels nitpicky. You know both values are “basically the same thing”. Rust disagrees—and it’s doing so for a very good reason.

Let’s break down what this error actually means and how to think about it like a Rustacean.

"Mismatched Types" in Rust

error[E0308]

What Rust Is Really Saying

“Mismatched types” means: “I inferred one concrete type here, but you gave me a different one there.”

Rust does not allow implicit type coercion between branches, return paths, or expressions. Every expression must resolve to one exact concrete type. This rule is foundational to Rust’s safety and performance guarantees.

Example 1: Branches Must Match

struct Cat;
struct Cow;

fn animal(sound: bool) {
    if sound {
        Cat
    } else {
        Cow
    }
}

❌ Error: expected Cat, found Cow

Why? Because if is an expression in Rust. Both branches must produce the same type.

Fix 1: Use an enum (zero-cost, explicit)

enum Animal {
    Cat(Cat),
    Cow(Cow),
}

fn animal(sound: bool) -> Animal {
    if sound {
        Animal::Cat(Cat)
    } else {
        Animal::Cow(Cow)
    }
}

This makes the design decision explicit and keeps everything statically typed.

Example 2: impl Trait Is Not Dynamic

trait Sound {
    fn noise(&self);
}

fn make_sound(flag: bool) -> impl Sound {
    if flag {
        Cat
    } else {
        Cow
    }
}

❌ Error: mismatched types

Despite appearances, impl Trait does not mean “any type implementing this trait”. It means: “One concrete type chosen by this function.”

Rust must know that type at compile time.

Fix: Use dynamic dispatch

fn make_sound(flag: bool) -> Box<dyn Sound> {
    if flag {
        Box::new(Cat)
    } else {
        Box::new(Cow)
    }
}

You trade a tiny runtime cost for flexibility.

Example 3: Iterators Bite Back

fn numbers(flag: bool) -> impl Iterator<Item = i32> {
    if flag {
        vec![1, 2, 3].into_iter()
    } else {
        0..3
    }
}

❌ Error: different iterator types

Even though both implement Iterator<Item = i32>, their concrete types differ (std::vec::IntoIter<i32> vs std::ops::Range<i32>).

Fix: Trait object or enum

fn numbers(flag: bool) -> Box<dyn Iterator<Item = i32>> {
    if flag {
        Box::new(vec![1, 2, 3].into_iter())
    } else {
        Box::new(0..3)
    }
}

Alternative: Use the either crate for zero-cost branching:

use either::Either;

fn numbers(flag: bool) -> impl Iterator<Item = i32> {
    if flag {
        Either::Left(vec![1, 2, 3].into_iter())
    } else {
        Either::Right(0..3)
    }
}

This keeps static dispatch while allowing different concrete types.

The Mental Model That Helps

When you see “mismatched types”, ask yourself:

  1. Is Rust expecting one concrete type here? Check if you’re returning different types from branches or match arms.
  2. Am I mixing values vs references? (T vs &T) This is a common source of confusion.
  3. Do I need compile-time certainty or runtime flexibility? Use enum or impl Trait for the former, dyn Trait for the latter.

Common Gotchas

String types

let s = if flag { "hello" } else { String::from("world") };

Error: &str vs String. Fix: convert both to the same type.

Option unwrapping

let x: i32 = Some(5);  // Error: expected i32, found Option<i32>

Fix: Use .unwrap(), .expect(), or pattern matching.

Final Takeaway

“Mismatched types” isn’t Rust being difficult—it’s Rust forcing you to make your design intent explicit.

Once you do, your code becomes:

  • Safer: No hidden conversions or type confusion
  • Faster: The compiler can optimize with full type information
  • Easier to reason about: Types document your intent

The compiler is your pair programmer. When it complains about mismatched types, it’s asking: “Are you sure this is what you meant?” Usually, taking a moment to clarify your intent leads to better code.

Uncategorized

Previous article

A Didactic dyn Trait ExampleNew!!