Error E0308

"mismatched types" — How to Fix

Fix Rust E0308 mismatched types by ensuring variable types align or explicitly converting values before assignment.

When the shapes don't match

You are writing a function to calculate a discount. You grab the price from a database, which comes back as a string because JSON is text-based. You try to multiply that string by 0.9. The compiler stops you with E0308: mismatched types. It is not being difficult. It is saving you from a runtime crash where your app tries to do math on the word "forty-two".

Rust treats types like physical connectors. A String is a specific shape. A u32 is a completely different shape. In Python or JavaScript, you can pass a string where an integer is expected, and the language tries to guess what you meant. Rust refuses to guess. If the shapes do not match, the code will not compile. You have to provide the adapter yourself. This explicitness prevents subtle bugs where a "100" string gets treated as the number 100 in one place but as text in another.

Rust does not guess. You wire the adapter.

Minimal example

The error appears the moment the compiler sees a value assigned to a variable or passed to a function with an incompatible type. The compiler compares the expected type against the found type and rejects the code.

fn main() {
    // The variable expects a u32 (unsigned 32-bit integer).
    let count: u32 = 10;

    // This fails. A string literal is type &str, not u32.
    // The compiler sees a structural mismatch immediately.
    let result: u32 = "10"; // E0308: mismatched types

    // Fix: Parse the string into a number explicitly.
    // parse() returns a Result because the string might not be a number.
    let parsed: u32 = "10".parse().unwrap();
    let result: u32 = parsed;
}

The error message tells you exactly what went wrong. It lists the expected type and the found type. If you see expected u32, found &str, the mismatch is clear. The compiler is pointing to the exact line where the shape breaks.

The error message is a map. Follow the expected versus found lines to locate the mismatch.

How the compiler checks types

When you write let x: u32 = "hello", the compiler performs a type check. It looks at the type annotation u32 and the expression "hello". It determines that "hello" is a &str (a string slice). It then compares the memory layout and capabilities of u32 and &str.

A u32 is 4 bytes of raw data. A &str is a pointer to memory plus a length. They are structurally incompatible. The compiler emits E0308. This check happens at compile time. There is no runtime cost for this safety. The error is a guarantee that the memory operations will be valid. If the code compiles, the types match, and the program can perform operations safely.

This strict checking also applies to function arguments. If a function expects a String and you pass a &str, the compiler rejects it. Rust does not automatically convert between types, even if the conversion seems obvious. Every conversion must be explicit. This forces you to think about ownership and data flow. You decide when to copy data, when to borrow it, and when to transform it.

Explicit conversions make the data flow obvious to anyone reading the code.

Realistic scenario: Parsing and ownership

In real applications, E0308 often appears when dealing with input data or struct construction. You might receive text from a user, a file, or an API, and need to store it in a structured format. The mismatch usually involves converting text to numbers, or handling the difference between owned strings and string slices.

/// Represents a user profile with an age.
struct User {
    name: String,
    age: u32,
}

/// Creates a user from raw input data.
fn create_user(name: &str, age_str: &str) -> User {
    // The struct requires an owned String for the name field.
    // The function receives a borrowed &str.
    // We convert the slice to an owned String by cloning the data.
    let owned_name = name.to_string();

    // The struct requires a u32 for age.
    // The function receives a &str.
    // parse() attempts to convert the text to a number.
    // It returns a Result, so we must handle the potential error.
    let age_num: u32 = age_str.parse().expect("Age must be a valid number");

    User {
        name: owned_name,
        age: age_num,
    }
}

fn main() {
    // This works. We provide the conversions explicitly.
    let user = create_user("Alice", "30");
    println!("User: {}, Age: {}", user.name, user.age);
}

Notice the use of .to_string() and .parse(). These are the standard adapters for common conversions. .to_string() creates an owned String from any type that implements Display. .parse() converts a string slice into a primitive type like u32, f64, or bool. Both methods are explicit. They make it clear that a transformation is happening.

The community prefers .to_string() over String::from() for conversions because it reads like an action. However, for literals, String::from("text") is slightly more explicit about allocation. When parsing, always check the Result in production code. Using .unwrap() or .expect() on user input is a recipe for panics. The convention is to return the error up the stack or use the ? operator to propagate it.

Read the "expected" and "found" lines. They tell you exactly where the shape breaks.

Common traps

E0308 appears in several patterns that trip up beginners. Recognizing these patterns helps you fix the error quickly.

String slices versus owned strings

The most frequent mismatch involves &str and String. A String owns its data on the heap. A &str is a borrowed view of string data. You cannot assign a String to a variable expecting &str, and you cannot assign a &str to a variable expecting String.

fn main() {
    let owned = String::from("Hello");

    // E0308: mismatched types.
    // The variable expects &str, but owned is String.
    // let slice: &str = owned;

    // Fix: Borrow the String to get a &str slice.
    let slice: &str = &owned;
}

The fix is usually adding an & to borrow the value, or calling .to_string() to take ownership. Think about whether the function needs to own the data or just read it. If it only reads, use &str. If it stores the data, use String.

Signed versus unsigned integers

Rust distinguishes between signed and unsigned integers strictly. You cannot assign a u32 to an i32 variable. The ranges differ. A u32 can hold values from 0 to 4 billion. An i32 can hold values from negative 2 billion to positive 2 billion. Assigning a u32 to an i32 could lose data if the value is too large.

fn main() {
    let unsigned: u32 = 100;

    // E0308: mismatched types.
    // expected i32, found u32
    // let signed: i32 = unsigned;

    // Fix: Convert safely using try_into().
    let signed: i32 = unsigned.try_into().expect("Value too large for i32");
}

Use .try_into() for safe conversion between integer types. It returns a Result that handles overflow errors. Use .as_i32() only if you are certain the value fits and you want a hard cast. Hard casts truncate silently. If you cast a u64 of 1000 to u8, you get 232. The compiler allows this because as is a promise that you checked the bounds. E0308 prevents accidental casts, but as bypasses that check.

Collections do not convert automatically

Rust does not convert collections automatically. If you have a Vec<String>, you cannot pass it to a function expecting Vec<&str>. Even though each element can be borrowed, the container types are distinct.

fn print_names(names: Vec<&str>) {
    for name in names {
        println!("{}", name);
    }
}

fn main() {
    let owned_names = vec![String::from("Alice"), String::from("Bob")];

    // E0308: mismatched types.
    // expected Vec<&str>, found Vec<String>
    // print_names(owned_names);

    // Fix: Convert the vector by iterating and borrowing each element.
    let borrowed_names: Vec<&str> = owned_names.iter().map(|s| s.as_str()).collect();
    print_names(borrowed_names);
}

The fix requires transforming the collection. You can use .iter().map() to convert each element, or change the function signature to accept a slice &[String] or &[&str] if possible. Slices are more flexible because they can view any contiguous sequence of data.

Type inference locks in early

Sometimes E0308 appears because the compiler inferred a type earlier in the code. If you create an empty vector, the compiler does not know the element type. It waits for the first usage to infer the type. Once inferred, the type is locked.

fn main() {
    // The compiler infers x as Vec<i32> based on the first push.
    let mut x = vec![];
    x.push(1);

    // E0308: mismatched types.
    // expected i32, found &str
    x.push("two");
}

The error points to the second push, but the root cause is the inference on the first push. Fix this by annotating the type explicitly when creating the variable. Use let mut x: Vec<String> = vec![]; if you intend to store strings.

Pick the conversion that matches your intent. Safety comes from knowing what you are transforming.

Decision: How to fix the mismatch

When you encounter E0308, choose the fix based on what you are trying to achieve. The goal is to make the types match while preserving the correct ownership and semantics.

Use .parse() when you have text input and need a primitive type like u32, f64, or bool.

Use .to_string() when you need to convert a value into an owned String for storage or return.

Use &value when a function expects a reference (&T) and you have an owned value (T).

Use value.as_ref() when you need to convert a smart pointer or wrapper into a reference for a trait bound.

Use .try_into() when converting between types that might lose data, like u64 to u32, and you want to handle the error gracefully.

Use .unwrap() or expect() only when you are certain the conversion will succeed, such as parsing a hardcoded constant or validated input.

Use type annotations like let x: Vec<String> = vec![]; when the compiler cannot infer the type and you need to guide inference early.

Choose the tool that expresses your intent clearly. The compiler will accept the code only when the types align perfectly.

Where to go next