Why do both branches of an if need the same type?
Short answer: because if is an expression. if cond { a } else { b } produces one value, and that
value has one type, so a and b have to agree. Nothing needs to consume the value for the rule to
apply — the compiler types both arms either way.
The whole if is one value
Section titled “The whole if is one value”In src/main.rs:
fn main() { let n = 3; let label = if n > 0 { "positive" } else { "negative" }; println!("{label}");}cargo run prints:
positiveif n > 0 { "positive" } else { "negative" } is a single expression of type &str. It isn’t a statement
that runs one of two blocks and then moves on, the way an if is in C — it’s a value computed down one of
two paths. let binds whatever comes back, so the right-hand side has to have exactly one type.
Disagree and it stops
Section titled “Disagree and it stops”fn main() { let score = 72; let pass = if score >= 60 { 1 } else { "fail" }; println!("{pass}");}cargo build prints:
error[E0308]: `if` and `else` have incompatible types --> src/main.rs:3:44 |3 | let pass = if score >= 60 { 1 } else { "fail" }; | - ^^^^^^ expected integer, found `&str` | | | expected because of this
For more information about this error, try `rustc --explain E0308`.Read the two marks: the caret is under the arm that broke the rule, the dash under the arm that set it.
Here 1 made the expression an i32, and "fail" is a &str. Either arm can change — 1 and 0 work,
"pass" and "fail" work — but they have to end up the same type, because let is binding one value.
Discarding the value doesn’t help
Section titled “Discarding the value doesn’t help”The obvious objection is that nobody asked for a value: the if was only there to choose which code runs.
So take the binding away.
fn main() { let score = 72; if score >= 60 { 1 } else { "fail" }; println!("done");}Same error, same wording, same two marks; only the column moves. The semicolon drops the value after the
expression has been typed, so the arms had to agree before anything got dropped. There’s no exemption for
an if whose value nobody wants.
One arm may diverge
Section titled “One arm may diverge”An arm that never produces a value fits beside any type. That’s what panic! returns — the never type,
!:
fn sign(n: i32) -> i32 { if n < 0 { panic!("negative: {n}") } else { n }}
fn main() { println!("{}", sign(4));}cargo run prints:
4panic!("negative: {n}") has type !, which coerces to whatever the surrounding expression needs, so this
if is an i32 and sign type-checks. return, break and continue work the same way. That’s how you
write a branch that fails instead of returning a number, without inventing a dummy number for it.
No else means a missing arm
Section titled “No else means a missing arm”If the expression has one type, a path through it that yields nothing can’t supply one. Drop the else
and the skipped case has only () to offer — so when a value is expected, the compiler reports it:
fn main() { let n = 3; let x = if n > 0 { 1 }; println!("{x}");}error[E0317]: `if` may be missing an `else` clause --> src/main.rs:3:13 |3 | let x = if n > 0 { 1 }; | ^^^^^^^^^^^-^^ | | | | | found here | expected integer, found `()` | = note: `if` expressions without `else` evaluate to `()` = help: consider adding an `else` block that evaluates to the expected type
For more information about this error, try `rustc --explain E0317`.Different code, same cause. Both errors above are the compiler saying the same thing: an if is one
expression, so every path through it has to produce a value of the one type it has.
Related
Section titled “Related”- How to return a value from a function — the tail expression that gives each arm its value
- Why would you shadow a variable instead of using mut? — the other place a type has to be settled up front
- Compiler errors — how to read a diagnostic, then one page per error code
- The bits that trip everyone up — the rest of this chapter
- Next in this chapter: how the range syntax decides what it includes