The shape mismatch
You write a function. You call it. The compiler rejects you with E0308. You look at the code. The types look identical. They aren't. One is a String, one is a &str. One is an i32, one is an i64. Rust doesn't care that they hold the same data. It cares about the shape of the type.
E0308 is the compiler's way of saying "I expected a box of this shape, you handed me a box of that shape." It is the most common error for new Rust developers. It appears in assignments, function arguments, return values, and struct fields. It stops compilation because Rust refuses to guess what you meant. It forces you to make the type match explicit.
Think of a function signature as a socket. It has a specific shape. If you try to plug a different shape into it, it won't fit. Rust checks the plug shape before you flip the switch. Python would let you plug anything in and hope it works. Rust refuses to compile until the shapes match. This check happens at compile time. It catches bugs before your code runs. It prevents subtle crashes where a value is interpreted incorrectly.
How type checking works
Rust is statically typed. Every value has a type known at compile time. Variables, expressions, and function parameters all have types. The compiler tracks these types as it analyzes your code. When it encounters an operation, it checks if the types are compatible.
Consider a simple addition.
fn main() {
let a: i32 = 5;
let b: i32 = 10;
let sum = a + b;
}
The compiler sees a is i32. It sees b is i32. The + operator for i32 returns i32. The types match. The code compiles.
Now change b to a different integer type.
fn main() {
let a: i32 = 5;
let b: i64 = 10;
let sum = a + b; // E0308: mismatched types
}
The compiler sees a is i32. It sees b is i64. The + operator requires both operands to have the same type. It cannot add an i32 and an i64 directly. The types mismatch. E0308 fires. The error message points to b and says "expected i32, found i64".
The compiler does not automatically convert i64 to i32. Conversion can lose data. An i64 can hold values larger than an i32. If the compiler silently truncated the value, you would lose information without knowing. Rust forces you to decide how to handle the conversion. You must choose whether to cast, widen, or restructure the code.
The String and &str trap
The most frequent source of E0308 for beginners is the difference between String and &str. In Python or JavaScript, strings are just strings. Rust splits strings into two types. String is an owned, growable heap allocation. &str is a borrowed slice pointing to UTF-8 data. The data might live on the heap, in static memory, or on the stack.
When you write a string literal like "hello", it has type &str. It lives in the binary. When you create a String, you allocate memory.
fn greet(name: &str) {
println!("Hello, {}", name);
}
fn main() {
let user = String::from("Alice");
greet(user); // E0308: expected &str, found String
}
The function greet expects a &str. You pass a String. The compiler rejects this. String owns data. &str borrows data. Passing a String would transfer ownership, but the function only wants a borrow. The types do not match.
The fix is to borrow the String.
fn greet(name: &str) {
println!("Hello, {}", name);
}
fn main() {
let user = String::from("Alice");
greet(&user); // OK: &String coerces to &str
}
Taking &user creates a reference to the String. Rust automatically dereferences the String to get the underlying &str. This is a safe coercion. The String remains owned by main. The function borrows the data. No allocation happens. No ownership transfers.
Convention aside: Function arguments should almost always take &str instead of String. Taking String forces the caller to allocate. Taking &str accepts both String and literals via auto-deref. It is more flexible and reduces E0308 errors for callers. Only take String when the function needs to own the data.
Integer literals and ambiguity
Integer literals like 5 have no fixed type. They can be i32, i64, u8, or any integer type. The compiler infers the type from context. If the context is ambiguous, you get E0308 or a type inference error.
fn main() {
let x = 5;
let y = 10u64;
let sum = x + y; // E0308: expected u64, found {integer}
}
The compiler sees y is u64. It tries to infer x. The + operator requires matching types. x must be u64. But x was initialized with 5 without a suffix. The compiler cannot decide if x should be u64 or something else before checking the addition. The inference fails. E0308 appears.
The fix is to annotate the type or add a suffix.
fn main() {
let x: u64 = 5;
let y = 10u64;
let sum = x + y; // OK
}
Or use a suffix on the literal.
fn main() {
let x = 5u64;
let y = 10u64;
let sum = x + y; // OK
}
This pattern shows up in arrays and vectors. All elements must have the same type.
fn main() {
let arr = [5, 10u64]; // E0308: expected u64, found {integer}
}
The compiler sees 10u64. It expects all elements to be u64. 5 is ambiguous. It cannot unify. Add the suffix or annotation.
fn main() {
let arr = [5u64, 10u64]; // OK
}
Trust the error message. It tells you the expected type and the found type. Match them.
When E0308 is a red herring
Sometimes E0308 points to the wrong location. The mismatch appears here, but the root cause is a missing type annotation there. This happens with generics and type inference.
fn process<T>(val: T) -> T {
val
}
fn main() {
let x = process(5);
let y = process(10u64);
let sum = x + y; // E0308: expected u64, found {integer}
}
The error points to x + y. It says x has an unknown integer type. The real issue is that process returns T. The compiler cannot infer T for x because x is not used in a context that constrains its type until the addition. By then, y is u64, and x is still ambiguous.
The fix is to annotate the variable or the call.
fn process<T>(val: T) -> T {
val
}
fn main() {
let x: u64 = process(5);
let y = process(10u64);
let sum = x + y; // OK
}
Annotating x resolves the inference chain. The compiler knows process returns u64. It infers T as u64 for the first call. The types match.
Don't fight the compiler here. Add the annotation. It clarifies your intent and helps the inference engine.
Conversions and casts
When types mismatch, you often need to convert. Rust provides several mechanisms. Each has different guarantees.
The .into() method is the preferred way to convert when the target type is clear. It uses the From and Into traits. These traits implement safe conversions.
fn main() {
let s = String::from("hello");
let bytes: Vec<u8> = s.into(); // OK: String implements Into<Vec<u8>>
}
The compiler sees Vec<u8> on the left. It calls .into() on String. The Into trait handles the conversion. This is safe. No data loss.
The as keyword performs a cast. It is unchecked and can lose data. Use it only for primitive numeric types where you accept the risk.
fn main() {
let big = 1000u16;
let small = big as u8; // small is 232. Data lost.
}
The value 1000 does not fit in u8. The cast truncates the bits. The result is 232. No warning. No error. The data is gone.
Convention aside: Avoid as for references. Casting &T to &U with as is almost always wrong. Use .into() or explicit constructors. The community treats as casts as a code smell unless you are doing low-level bit manipulation or performance-critical math where you have verified the values fit.
When conversion might fail, use .try_into(). It returns a Result. You must handle the error.
fn main() {
let big = 1000u16;
let small: Result<u8, _> = big.try_into();
match small {
Ok(val) => println!("Fits: {}", val),
Err(_) => println!("Too big"),
}
}
This is the safe way to narrow types. You cannot ignore the failure. The compiler forces you to handle it.
Decision matrix
Use &value when you have an owned value and the context expects a reference. This borrows the data without transferring ownership. It works for String to &str, Vec<T> to [T], and any type implementing Deref.
Use .as_str() when converting a String to a &str to satisfy a borrow. This is explicit and clear. It signals that you are borrowing the string data. Prefer this over &value when the intent is specifically about strings.
Use .into() when the target type is clear from context and you want the compiler to handle the conversion via From. This is the idiomatic way to convert between types. It is safe and flexible. It works for String to Vec<u8>, &str to String, and many other pairs.
Use .try_into() when the conversion might fail and you need to handle the error. This is required for narrowing numeric casts, parsing, and any conversion that is not guaranteed to succeed. It returns a Result that you must match or unwrap.
Use as only for primitive numeric casts where you accept potential data loss and have verified the value fits. Never use as for references or complex types. It bypasses safety checks. It is a tool for experts, not beginners.
Use explicit type annotations when the compiler cannot infer a type and you get a cascade of E0308 errors. Annotations break the ambiguity. They guide the inference engine. Add them to variables, function parameters, or return types as needed.
Pitfalls to avoid
Integer overflow in casts is silent. as does not check bounds. If you cast a large integer to a small type, the value wraps. This causes bugs that are hard to trace. Use .try_into() for safety.
Confusing String and &str leads to lifetime errors. E0308 often appears before E0597. Fix the type mismatch first. Then check lifetimes. The errors can cascade.
Generic type inference can hide mismatches. If you use a generic function, the compiler might delay type checking until the value is used. The error appears far from the source. Trace back to the generic call. Add annotations if needed.
Closures with multiple return paths must return the same type. If one branch returns String and another returns &str, you get E0308. Ensure all branches return the same type. Convert as needed.