The error that greets you at the door
You're refactoring a helper function. You swap a String for a &str to avoid an allocation. You hit cargo build. The terminal floods with red text. The code looks identical to what you wrote five minutes ago. The compiler disagrees.
E0308 is the error you'll see more than any other in Rust. It's the compiler's way of saying, "I know what you meant, but you wrote something else." The message is loud, but it's also precise. Once you learn to read the two key lines, the fix is almost always mechanical.
Types are shapes, not labels
Rust treats types like physical shapes. You can't shove a triangle into a square slot. Every expression in Rust has a concrete type. When you assign a value, return from a function, or pass an argument, the shape of the value must match the shape of the slot. If they don't match, the compiler stops you with E0308.
The error message always gives you two pieces of data: the expected shape and the found shape. Your job is to decide which one is wrong. Sometimes the slot is wrong because you copied a signature from an old API. Sometimes the value is wrong because you forgot a method call. The compiler doesn't care about your intent. It only cares about the shapes.
The smallest mismatch
Here's the minimal case: a pattern that doesn't match the value.
fn main() {
// Pattern expects a 2-tuple.
// Value provides a 3-tuple.
// Mismatch: expected 2 elements, found 3.
let (x, y) = (1, 2, 3);
}
The pattern (x, y) describes a container with two slots. The value (1, 2, 3) is a container with three slots. Rust sees the mismatch immediately. The compiler rejects this with E0308, pointing out that it expected a tuple of length 2 but found a tuple of length 3.
The fix is one of three paths. Add a binding to consume the third element. Ignore the third element with an underscore. Or change the value to match the pattern. Pick the one that matches your logic.
The semicolon trap
Newcomers trip over semicolons constantly. In Rust, a semicolon turns an expression into a statement. An expression produces a value. A statement produces nothing, which Rust calls the unit type ().
Here's the trap in action.
fn add(a: i32, b: i32) -> i32 {
// Semicolon turns the addition into a statement.
// The function body now returns `()` instead of i32.
a + b;
}
The function signature promises to return i32. The body returns (). The compiler screams E0308: expected i32, found (). The fix is to drop the semicolon on the last line. The value returns itself.
Rust developers call this the semicolon trap. The community convention is to never put a semicolon on the last expression of a function body unless you explicitly want to return (). If the compiler complains about () where you expected a value, check for a rogue semicolon first.
References versus values
Rust distinguishes between owning a value and borrowing a reference. A function that takes String wants ownership. A function that takes &str wants a borrow. Mixing them up triggers E0308.
Here's the mismatch.
fn consume(s: String) {
println!("{s}");
}
fn main() {
let name = String::from("Ferris");
// Function wants ownership of String.
// Caller passes a reference.
consume(&name);
}
The compiler reports E0308: expected String, found &String. The caller is handing over a reference, but the function demands the full value. You can fix this by calling .clone() to give the function its own copy, or by removing the & to move ownership.
Better yet, change the function signature. If the function only reads the string, it shouldn't take ownership. Change String to &str.
fn read(s: &str) {
println!("{s}");
}
fn main() {
let name = String::from("Ferris");
// &str accepts both String and string literals.
// No allocation, no ownership transfer.
read(&name);
}
Taking &str is the community standard for read-only string parameters. It accepts both String and string literals without forcing the caller to allocate. If you see &String in a signature, it's usually a mistake. Reach for &str.
Numeric types don't mix
Rust does not silently convert between numeric types. u32 and usize are distinct types, even on a 64-bit system where they happen to be the same size. i32 and f64 are distinct. Adding them together is an error.
Here's the mismatch.
fn main() {
let a: u32 = 10;
let b: usize = 20;
// u32 and usize are different types.
// Rust refuses to guess the conversion.
let sum = a + b;
}
The compiler rejects this with E0308: expected u32, found usize. You must be explicit. Use as for a cheap cast, or use try_into() for a checked conversion.
fn main() {
let a: u32 = 10;
let b: usize = 20;
// as converts usize to u32.
// Truncates if the value is too large.
let sum = a + (b as u32);
}
The community avoids as in public APIs because it hides potential data loss. Use as when you have verified the value fits and performance matters. Use try_into() when the value might overflow and you need to handle the failure. Be explicit. Rust won't guess your intent.
If arms must match
When you use an if expression as a value, both branches must produce the same type. This is a common source of E0308 for developers coming from languages where branches can return different types.
Here's the mismatch.
fn main() {
let n = 5;
// If expression returns a value.
// Both arms must produce the same type.
let label = if n > 0 {
"positive" // &str
} else {
n // i32
};
}
The compiler reports E0308: if and else have incompatible types. One arm produces &str, the other produces i32. Rust needs a single type for the variable label.
Pick a type and produce it in both arms. If you need a string, convert the number.
fn main() {
let n = 5;
// Both arms produce &str.
// Types match, no error.
let label = if n > 0 {
"positive"
} else {
"non-positive"
};
}
Or use format! to produce a String in both arms. Consistency is key. The compiler won't coerce one type into another just to make the arms match.
Realistic parsing: Options and conversions
Real code often involves parsing data where the type isn't guaranteed. JSON parsers return Option types. Database drivers return Result types. Forgetting to unwrap or convert triggers E0308.
Here's a realistic scenario.
use serde_json::Value;
/// Extracts an age from a JSON record.
fn get_age(record: &Value) -> u32 {
// as_u64 returns Option<u64>.
// Function signature expects u32.
// Two mismatches: Option wrapper and integer size.
record["age"].as_u64()
}
The compiler reports E0308: expected u32, found Option<u64>. There are two problems on one line. The value is wrapped in an Option, and the inner type is u64, not u32.
The fix requires handling both layers. Unwrap the option, then convert the integer.
use serde_json::Value;
/// Extracts an age from a JSON record, defaulting to zero.
fn get_age(record: &Value) -> u32 {
// as_u64 returns Option<u64>.
// unwrap_or extracts u64 or returns default.
// try_into attempts conversion to u32.
// unwrap_or handles overflow by capping at max.
record["age"]
.as_u64()
.unwrap_or(0)
.try_into()
.unwrap_or(u32::MAX)
}
The compiler forces you to handle the absence and the size. That's not a bug. That's safety. If the age is missing, you get zero. If the age is too large, you get the maximum u32. The function never panics on bad input.
Reading the error message
The E0308 message has a rhythm. Look for the carets. They point to the exact token causing the clash. Look for the "expected" and "found" lines. The "expected" type comes from the context: the variable type, the function return, the parameter. The "found" type comes from your code.
If the "expected" type looks wrong, the bug is upstream. You might have annotated a variable with the wrong type, or a function signature is stale. If the "found" type looks wrong, the bug is right there. You might have dropped a method call, or passed a reference instead of a value.
Sometimes the "found" type is (). That means you dropped a value with a semicolon. Sometimes the "found" type is &T when T was expected. That means you passed a reference instead of a value. Sometimes the "found" type is a long generic chain. That means the compiler inferred a complex type and you need to annotate an intermediate variable to break the chain.
Rustaceans often say the compiler is your friend. E0308 is the friend that points out your typos before they become runtime crashes. Treat the error message as a specification. It tells you exactly what the compiler needs to proceed.
When to use what
Use explicit type annotations when the compiler's inferred type is too generic or the error spans multiple lines. Annotating an intermediate variable forces the compiler to commit early and makes the error more local.
Use as for numeric conversion when you have verified the value fits the target type and performance matters. This is common in inner loops where bounds are guaranteed by logic.
Use try_into() for numeric conversion when the value might overflow and you need to handle the failure gracefully. This is the standard for parsing external data.
Use .clone() when the consumer needs ownership and you can afford the allocation. This is common when passing data to a new thread or storing it in a collection.
Use &str instead of &String in function signatures to accept both string literals and owned strings without forcing the caller to allocate. This is the community convention for read-only string parameters.
Use pattern matching with ref or & when destructuring borrowed data to avoid moving values out of references. This keeps the borrow checker happy when you need to inspect parts of a structure without taking ownership.