Quick answer
Rust's type checker found two incompatible types at a point where one specific type was required. The compiler always prints both an expected type and a found type — read those two lines first. The five real-world causes are integer-width mismatches, &T vs owned T, an unhandled Option/Result wrapper, String vs &str, and mismatched if/match branches.
The exact error string
error[E0308]: mismatched types
--> src/main.rs:4:20
|
4 | let x: i32 = get_count();
| --- ^^^^^^^^^^^ expected `i32`, found `u64`
| |
| expected due to this
|
help: you can convert a `u64` to an `i32` and panic if the converted
value doesn't fit
|
4 | let x: i32 = get_count().try_into().unwrap();
| ++++++++++++++++++++
Every E0308 error follows this shape: a caret pointing at the exact expression, an expected <type>, found <type> line naming both sides precisely, and very often a help: suggestion with the exact conversion call to insert. Reading the expected/found pair first — before scrolling past it to reason about the code yourself — is the fastest way to diagnose which of the five causes below applies.
Cause 1: two different integer types (no implicit numeric coercion)
fn get_count() -> u64 { 42 }
let x: i32 = get_count(); // ❌ expected i32, found u64
// ✅ checked conversion — returns a Result, panics on overflow with .unwrap()
let x: i32 = get_count().try_into().unwrap();
// ✅ or an unchecked cast — silently truncates on overflow, use with care
let x: i32 = get_count() as i32;
Rust has no automatic widening or narrowing between distinct integer types — not even from a smaller type to a strictly larger one that could hold every value, like i32 to i64. This is a deliberate departure from C-family languages: implicit numeric coercion is a well-documented source of silent bugs (a narrowing conversion truncating a value with no visible signal at the call site), and Rust's design philosophy treats "this could lose data" as something that must be spelled out explicitly, either as a checked try_into() that forces you to handle failure, or an explicit as cast that visibly marks the truncation as an accepted risk.
Cause 2: a reference where an owned value was expected (or vice versa)
fn print_name(name: String) { println!("{name}"); }
let n = String::from("Ada");
print_name(&n); // ❌ expected `String`, found `&String`
// ✅ pass the owned value (moves it), or clone if you still need `n` after:
print_name(n);
print_name(n.clone()); // if `n` is used again later
A &String (a borrowed reference) and a String (an owned value) are distinct types with different implications for ownership, and a function signature that specifically asks for the owned type won't accept a reference to it, even though the underlying data looks identical. This is where Rust's ownership model surfaces directly in the type system: the compiler is really asking "do you want to hand over ownership of this value, or just lend a look at it," and the fix depends on which one the function actually needs.
Cause 3: an unhandled Option<T> or Result<T, E> passed where T was expected
fn find_user(id: u32) -> Option<String> { /* ... */ None }
let name: String = find_user(1); // ❌ expected `String`, found `Option`
// ✅ unwrap deliberately — panics if None, only for cases you're SURE succeed:
let name: String = find_user(1).unwrap();
// ✅ or handle both cases explicitly (the idiomatic default):
let name: String = match find_user(1) {
Some(n) => n,
None => "unknown".to_string(),
};
Option<T> and Result<T, E> are wrapper types, not T itself with an asterisk — the compiler treats Option<String> and String as completely unrelated types for the purposes of type-checking, the same as it would treat Vec<String> and String as unrelated. This is the type-system-level enforcement of Rust's "no null, no silent failure" philosophy: you cannot accidentally use a possibly-absent value as if it were guaranteed present, because the wrapper type itself blocks it at compile time until you explicitly decide what happens in the absent/error case.
Cause 4: String vs &str at a boundary that only accepts one
fn store_owned(s: String) { /* takes ownership, e.g. stores in a struct */ }
let greeting = "hello"; // &str — a string literal
store_owned(greeting); // ❌ expected `String`, found `&str`
// ✅ allocate an owned String from the borrowed &str:
store_owned(greeting.to_string());
store_owned(greeting.to_owned());
store_owned(String::from(greeting));
&str is a borrowed view into string data that Rust doesn't own at that point (a literal baked into the binary, or a slice of someone else's String), while String is an owned, heap-allocated, growable buffer. Functions that only need to read a string typically accept &str (and Rust's deref coercion conveniently lets you pass a &String there too), but a function that needs to keep the string — store it in a struct field, return it, push it into a Vec<String> — must ask for an owned String, forcing the explicit allocation that a plain reference can't provide.
Cause 5: if/else or match branches that don't all produce the same type
let label = if count == 1 {
"one item" // &str
} else {
format!("{count} items") // ❌ String — expected &str, found String
};
// ✅ make both branches produce the same type
let label = if count == 1 {
"one item".to_string()
} else {
format!("{count} items")
};
An if/else (or match) is itself a single expression with one overall type, computed by unifying every branch's type into one — this is what lets Rust support expression-oriented patterns like let x = if cond { a } else { b }; at all. Each branch is type-checked individually first and looks perfectly valid in isolation; the mismatch only becomes visible once the compiler tries to reconcile all branches into the one type the whole expression must have.
Common causes at a glance
| Expected / found pair | Root cause | Fix |
|---|---|---|
i32 / u64 (or any two integer types) | no implicit numeric coercion between distinct integer types | .try_into() (checked) or as (unchecked cast) |
String / &String | owned value required, reference supplied | pass the owned value, or .clone() |
String / Option<String> | wrapper type not unwrapped | .unwrap(), match, or the ? operator |
String / &str | owned value needed at a boundary, borrowed slice supplied | .to_string() / .to_owned() |
same pair, inside an if/match | branches don't produce identical types | make every branch return the same type |
Why this happens: Rust's type system has no implicit coercion by design
Most of the causes above trace back to a single underlying design principle: Rust deliberately minimizes implicit type conversions, treating every conversion — numeric narrowing, borrowing vs owning, unwrapping a fallible/optional value — as something the programmer must request explicitly rather than something the compiler infers on your behalf. Languages with generous implicit coercion (C's numeric promotions, JavaScript's type juggling) trade type-safety for convenience, and that convenience is exactly where a large fraction of runtime bugs in those languages originate — a silent truncation, an unexpected string-to-number coercion, a null where a value was assumed. Rust's E0308 is the compiler catching precisely those moments and demanding an explicit decision instead, which is why nearly every fix above is "call the specific conversion function" rather than "adjust some compiler flag" — the type mismatch is the language successfully doing its job, not an obstacle around it.
Debugging checklist
- ✓ Read the
expected/foundline first — it names both types exactly - ✓ Check the
help:suggestion below the error — rustc often names the exact conversion to insert - ✓ Two integer types? Use
.try_into()(checked) oras(explicit truncation) - ✓
Option/Resultinvolved? Unwrap ormatchit before using the inner value - ✓ Inside an
if/match? Confirm every single branch returns the identical type
Frequently Asked Questions
What does "error[E0308]: mismatched types" mean?
Rust's type checker determined that a value of one specific type was supplied where a different specific type was required — a function parameter, a variable's declared type, a match arm, or a return type all fixed an expectation the actual value didn't satisfy. The compiler always names both the "expected" and "found" type explicitly.
Why doesn't Rust automatically convert between integer types like i32 and u64?
Rust deliberately has no implicit numeric coercion between distinct integer types, unlike C or Java, because a silent narrowing conversion (e.g. i64 to i32) can silently truncate a value, and Rust's core design goal is to make every possible source of undefined or surprising behavior an explicit, visible choice in the code. You must call an explicit conversion — try_into() (checked, returns a Result) or as (unchecked, truncates silently) — so the possibility of data loss is visible at the call site.
Why does the compiler expect &str but I passed a String, or vice versa?
String is an owned, heap-allocated, growable string, while &str is a borrowed, fixed-size view into string data (owned by something else, or a string literal baked into the binary). A function that takes &str can accept both a string literal and a borrowed String directly (via automatic deref coercion, &my_string becomes &str), but a function that specifically requires an owned String needs .to_string() or .to_owned() called on a &str to actually allocate one.
Why does this happen inside an if/else or match even though each branch looks fine alone?
Every branch of an if/else expression (and every arm of a match) must produce the exact same type, because the whole expression itself has one single type, determined by unifying all its branches. If one branch returns a String and another returns a &str literal, that mismatch is only visible when the compiler tries to reconcile the two branches into one overall expression type.
Why does E0308 show up when I forget to handle an Option or Result?
A function that returns Option<T> gives you a wrapper type, not a bare T with an asterisk — passing that wrapper directly to something expecting a plain T is a type mismatch between Option<T> and T, not a special case of "missing a value." You must unwrap it deliberately (via unwrap(), expect(), a match, or the ? operator) to extract the T before using it as one.
Does the compiler suggest a fix for E0308?
Very often, yes — rustc's diagnostics for E0308 frequently include a "help:" line suggesting the exact missing conversion (e.g. "try using a conversion method: .to_string()" or "consider borrowing here"). Reading past the first two lines of the error to the help text resolves a large fraction of these without needing to reason through the type system manually.
More Go, Rust & Java errors
Browse the full reference for Go, Rust, Java, and other backend language errors — exact message, cause, and fix.