If / Else
Basics · Lesson 10
if needs no parentheses around the condition. It is also an expression, so it can produce a value.
Coming from Goif looks like Go's: no parentheses, braces required, and the condition must be a bool. It is also an expression, so let size = if n > 5 { "big" } else { "small" }; replaces declaring size first and assigning in a branch.
Coming from KotlinThis is Kotlin's if expression without the parentheses, and the braces are required. Both branches must give the same type, where Kotlin would quietly pick a common supertype.
Coming from Pythonif with a value replaces "big" if n > 5 else "small". The condition must be a real bool: there is no truthiness, so if n with a number or if items with a list won't compile.
Coming from TypeScriptif with a value replaces the ternary n > 5 ? "big" : "small". The condition must be a real bool, so if n with a number or if s with a string won't compile, and both branches must give the same type.
Coming from JavaRust's if is an expression, so let size = if n > 5 { "big" } else { "small" }; replaces the ?: ternary, which Rust does not have. Rust never converts one branch to fit the other, where Java's ternary would pick a common supertype or promote a number.
Coming from C#if gives back a value, so it replaces C#'s ?:, which Rust does not have. Like C#, the condition must be a bool; unlike C#'s ?:, Rust never converts one branch to fit the other. Rust also drops the parentheses but always needs the braces.
Coming from CRust's if is an expression that replaces ?:, which Rust does not have. The condition must be a real bool (no if (n) on an integer or pointer), and both branches must have the same type, so a 0 can't stand in for a null string.
Coming from C++if is an expression, so it does the job of ?:, which Rust does not have. The condition must be a bool, with no implicit conversion from integers or pointers, and both branches must have exactly the same type.
The quick comparison
fn main() { let n = 7; if n % 2 == 0 { println!("{n} is even"); } else { println!("{n} is odd"); }
// `if` gives back a value, so Rust has no `?:` let size = if n > 5 { "big" } else { "small" }; println!("{size}");}7 is odd big
package main
import "fmt"
func main() { n := 7 if n%2 == 0 { fmt.Println(n, "is even") } else { fmt.Println(n, "is odd") }
size := "small" if n > 5 { size = "big" } fmt.Println(size)}7 is odd big
fun main() { val n = 7 if (n % 2 == 0) { println("$n is even") } else { println("$n is odd") }
val size = if (n > 5) "big" else "small" println(size)}7 is odd big
n = 7if n % 2 == 0: print(f"{n} is even")else: print(f"{n} is odd")
# the conditional expression gives back a valuesize = "big" if n > 5 else "small"print(size)7 is odd big
const n = 7;if (n % 2 === 0) { console.log(`${n} is even`);} else { console.log(`${n} is odd`);}
// `if` is a statement in TypeScript; the ternary gives back a valueconst size = n > 5 ? "big" : "small";console.log(size);7 is odd big
public class Main { public static void main(String[] args) { int n = 7; if (n % 2 == 0) { System.out.println(n + " is even"); } else { System.out.println(n + " is odd"); }
// the ternary operator is Java's if-as-a-value String size = n > 5 ? "big" : "small"; System.out.println(size); }}7 is odd big
var n = 7;if (n % 2 == 0){ Console.WriteLine($"{n} is even");}else{ Console.WriteLine($"{n} is odd");}
// the ?: operator gives back a valuevar size = n > 5 ? "big" : "small";Console.WriteLine(size);7 is odd big
#include <stdio.h>
int main(void) { int n = 7; if (n % 2 == 0) { printf("%d is even\n", n); } else { printf("%d is odd\n", n); }
// C's if does not give back a value; the ?: operator does const char *size = n > 5 ? "big" : "small"; printf("%s\n", size); return 0;}7 is odd big
#include <iostream>#include <string_view>
int main() { int n = 7; if (n % 2 == 0) { std::cout << n << " is even\n"; } else { std::cout << n << " is odd\n"; }
// the ternary operator gives back a value std::string_view size = n > 5 ? "big" : "small"; std::cout << size << "\n";}7 is odd big
What's different
- The condition must be a
bool.if n { }with a number does not compile. There is no "truthy" zero, so writeif n != 0. - There is no ternary
a ? b : c.if a { b } else { c }is the same thing. - Chain tests with
else if. When every branch compares one value against fixed cases,match(a later lesson) is usually clearer. if letandlet ... elsetest whether a value has a given shape and name its parts in the same step. They replace "check, then unpack".&&and||stop as soon as the answer is known, so the right side runs only when it is needed.
Two branches that give different types
Looks fine: in Kotlin, Python, TypeScript, Java, C and C++ it runs fine. Rust won't compile it.
No direct equivalent in Go. Just a Rust rule.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.C# refuses this too. Same idea, different words.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.fn main() { let n = 7; let size = if n > 5 { "big" } else { 0 }; println!("{size}");}Go has no if expression, so this can't come up. You declare size with one type first and assign to it in each branch.
fun main() { val n = 7 val size = if (n > 5) "big" else 0 println(size)}big
n = 7size = "big" if n > 5 else 0print(size)big
const n = 7;const size = n > 5 ? "big" : 0; // type: "big" | 0console.log(size);big
public class Main { public static void main(String[] args) { int n = 7; var size = n > 5 ? "big" : 0; System.out.println(size); }}big
C# refuses this too: the two sides of ?: must share a type, and there is no conversion between string and int (CS0173).
var n = 7;var size = n > 5 ? "big" : 0;Console.WriteLine(size);Program.cs(2,12): error CS0173: Type of conditional expression cannot be determined because there is no implicit conversion between 'string' and 'int'
#include <stdio.h>
int main(void) { int n = 7; const char *size = n > 5 ? "big" : 0; printf("%s\n", size); return 0;}big
#include <iostream>
int main() { int n = 7; auto size = n > 5 ? "big" : 0; std::cout << size << "\n";}big
What does the compiler say? Guess first, then open.
error[E0308]: `if` and `else` have incompatible types --> src/main.rs:3:42 | 3 | let size = if n > 5 { "big" } else { 0 }; | ----- ^ expected `&str`, found integer | | | expected because of this
Go has no if expression, so this can't come up. You declare size with one type first and assign to it in each branch.
Kotlin runs this and prints big. size quietly gets a loose common type instead of String, so later code can't use it as text without a check.
Python runs this and prints big. The two branches can give different types, so size might be a string or an int and later code has to cope with both.
Prints big. TypeScript infers the union "big" | 0 and lets it through; every later use of size as a string then needs a typeof check or the compiler rejects it. Rust asks you to say so up front instead.
Java compiles this and prints big. It picks a shared supertype for the two branches, so size ends up as a vague Object-like type that later code can't do much with.
C# refuses this too: the two sides of ?: must share a type, and there is no conversion between string and int (CS0173).
C accepts this because a plain 0 counts as a null pointer. It prints big here, but with a smaller n the string would be NULL.
This compiles because a literal 0 also counts as a null pointer, so both branches become const char*. It prints big, but if n were small cout would get a null pointer.
Rust gives every variable exactly one type, decided while compiling. size cannot be "a string or maybe a number".
Code after this line uses size. If it could be either type, every later line would need to check which one it got. Rust makes both branches agree up front instead. If you really want "string or number", that is an enum, covered later.
The fix
Compiles and runs.
fn main() { let n = 7; let size = if n > 5 { "big" } else { "small" }; println!("{size}");}big
The toolkit
Statements and expressions showed how if looks, as a value, with the stray ; and the if x = 1 typo. This page is about how it behaves. Outputs come from Linux on 64-bit ARM with Rust 1.99.1.
if is an expression, and both arms agree
An if/else has a value, so it can sit wherever a value can (Statements and expressions has the rules). A branch may be a whole block; its last expression, with no ;, is the value.
fn main() { let n = 7;
// `if` as the value of a `let` let size = if n > 5 { "big" } else { "small" }; println!("size: {size}");
// `if` as an argument, with no temporary variable println!("parity: {}", if n % 2 == 0 { "even" } else { "odd" });
// a block as a branch: its last expression is the value let label = if n > 5 { let doubled = n * 2; doubled + 1 } else { 0 }; println!("label: {label}");}size: big parity: odd label: 15
Every branch must give the same type, because limit gets exactly one. Rust will not widen an integer to a float to match:
fn main() { let n = 7; let limit = if n > 5 { 1 } else { 2.5 }; println!("limit: {limit}");}error[E0308]: `if` and `else` have incompatible types --> src/main.rs:3:39 | 3 | let limit = if n > 5 { 1 } else { 2.5 }; | - ^^^ expected integer, found floating-point number | | | expected because of this
The fix is 1.0: an integer and a float are different types, not one number. A branch that ends in return, continue or panic! is exempt, because it never gives a value (the never type). An if with no else gives (), so it cannot supply a value (E0317, in Smaller slips).
Only a bool is a condition
There is no truthiness: a number, a string, a list or an Option is not a condition (E0308, “expected bool”). The number case is in Smaller slips and the others are on What Rust lacks. Write the comparison you mean: n != 0, !name.is_empty(), found.is_some().
Comparing and combining
fn main() { let (a, b) = (3, 8); println!("a == b: {}", a == b); println!("a != b: {}", a != b); println!("a < b: {}", a < b); println!("a >= b: {}", a >= b); println!("a > 1 && b > 1: {}", a > 1 && b > 1); println!("a > 5 || b > 5: {}", a > 5 || b > 5); println!("!(a < b): {}", !(a < b)); // `&` binds tighter than `==` in Rust, so no parentheses are needed here println!("a & 1 == 1: {}", a & 1 == 1);}a == b: false a != b: true a < b: true a >= b: false a > 1 && b > 1: true a > 5 || b > 5: true !(a < b): false a & 1 == 1: true
== and != compare contents (numbers, text, tuples, arrays and many other types); <, >, <= and >= need a type with an order. Two numbers must have the same type, with no implicit widening (Numbers and types). A few mixed pairs, such as String and &str, are written to be comparable.
fn main() { let small: i32 = 1; let big: i64 = 1; if small == big { println!("equal"); }}error[E0308]: mismatched types --> src/main.rs:4:17 | 4 | if small == big { | ----- ^^^ expected `i32`, found `i64` | | | expected because this is `i32` | help: you can convert `small` from `i32` to `i64`, matching the type of `big` | 4 | if i64::from(small) == big { | ++++++++++ +
Comparisons do not chain:
fn main() { let x = 3; if 1 < x < 5 { println!("between"); }}error: comparison operators cannot be chained --> src/main.rs:3:10 | 3 | if 1 < x < 5 { | ^ ^ | help: split the comparison into two | 3 | if 1 < x && x < 5 { | ++++
&& and || take bools. ! is “not” on a bool; on an integer it flips every bit instead, so write n == 0, not !n (Go spells the bit flip ^n). && and || short-circuit: the right side runs only if the left has not already decided the answer, which makes !items.is_empty() && items[0] > 5 safe. & and | also work on bool but run both sides:
fn check(name: &str, answer: bool) -> bool { println!(" checked {name}"); answer}
// the right side indexes the slice, so it must only run when the slice is not emptyfn first_is_big(items: &[i32]) -> bool { !items.is_empty() && items[0] > 5}
fn main() { println!("false && ...:"); let a = check("left", false) && check("right", true); println!(" result: {a}");
println!("true || ...:"); let b = check("left", true) || check("right", false); println!(" result: {b}");
println!("false & ... (no short-circuit):"); let c = check("left", false) & check("right", true); println!(" result: {c}");
println!("first_is_big(&[]): {}", first_is_big(&[])); println!("first_is_big(&[9, 1]): {}", first_is_big(&[9, 1]));}false && ...: checked left result: false true || ...: checked left result: true false & ... (no short-circuit): checked left checked right result: false first_is_big(&[]): false first_is_big(&[9, 1]): true
(&[] and &[9, 1] are borrowed arrays: Borrowing.)
else if, and when to use match instead
if/else if/else tests in order and takes the first branch that is true. When the chain is the function’s value, as here, the last else is required, or the chain fails with the E0317 named in the first section (message in Smaller slips).
fn describe(n: i32) -> &'static str { if n < 0 { "negative" } else if n == 0 { "zero" } else if n < 10 { "small" } else { "large" }}
fn main() { for n in [-5, 0, 3, 10, 99] { println!("{n}: {}", describe(n)); }}-5: negative 0: zero 3: small 10: large 99: large
(&'static str is the type of a string literal: Reading signatures.)
When the branches compare one value with fixed cases or ranges, match is more direct and the compiler checks that no case is missing. A chain of ifs does not; use it when the tests are different questions.
if let and let ... else
if let asks “does this value have this shape?” and unpacks it in one step. The pattern goes left of the =. Here the value is the Result that parse gives back: Ok(number) or Err(reason) (Errors covers it). parse also rejects a number that does not fit the type, as "99999999999" shows for i32. Option (Option) and any enum (Enums) work the same way.
fn main() { // `parse` returns a Result: Ok(number) or Err(reason). Errors are covered later. for text in ["42", "4x2", "99999999999"] { if let Ok(n) = text.parse::<i32>() { println!("{text:?} is the number {n}"); } else { println!("{text:?} is not a number"); } }
// `else if let` chains several patterns let text = "7"; if let Ok(n) = text.parse::<u8>() { println!("fits in a u8: {n}"); } else if let Ok(n) = text.parse::<i64>() { println!("fits in an i64: {n}"); } else { println!("not a whole number"); }}"42" is the number 42 "4x2" is not a number "99999999999" is not a number fits in a u8: 7
The names a pattern binds exist only inside the if let block:
fn main() { if let Ok(v) = "5".parse::<i32>() { println!("inside: {v}"); } println!("outside: {v}");}error[E0425]: cannot find value `v` in this scope --> src/main.rs:5:25 | 5 | println!("outside: {v}"); | ^ not found in this scope
let ... else is for the other case: the value must have the shape, or the code leaves this spot. The binding stays in scope after the statement, so the happy path stays unindented. The else block must leave (for example return, break, continue or panic!):
fn double(text: &str) -> i32 { let Ok(n) = text.parse::<i32>() else { println!(" cannot parse {text:?}, using 0"); return 0; }; // `n` is in scope for the rest of the function, not inside a block n * 2}
fn main() { println!("double(\"21\"): {}", double("21")); println!("double(\"abc\"): {}", double("abc"));}double("21"): 42
cannot parse "abc", using 0
double("abc"): 0fn main() { let text = "abc"; let Ok(n) = text.parse::<i32>() else { println!("not a number"); }; println!("n: {n}");}error[E0308]: `else` clause of `let...else` does not diverge --> src/main.rs:3:42 | 3 | let Ok(n) = text.parse::<i32>() else { | __________________________________________^ 4 | | println!("not a number"); 5 | | }; | |_____^ expected `!`, found `()` | = note: expected type `!` found unit type `()` = help: try adding a diverging expression, such as `return` or `panic!(..)` = help: ...or use `match` instead of `let...else`
(Declarations and types shows the syntax.) Use if let when a mismatch means “do nothing”, let ... else when it means “stop here”. Both throw away the Err payload; to keep it, use match (Errors).
Comparing floats
Floats follow IEEE 754, as in most languages: results are not exact, and NaN (“not a number”) is neither smaller, equal nor larger than anything. Rust’s own point is the else.
fn main() { let sum = 0.1 + 0.2; println!("0.1 + 0.2 == 0.3: {}", sum == 0.3); println!("sum: {sum}"); println!("close enough: {}", (sum - 0.3_f64).abs() < 1e-9);
let nan = f64::NAN; println!("nan == nan: {}", nan == nan); println!("nan < 0.0: {}", nan < 0.0); println!("nan > 0.0: {}", nan > 0.0); println!("nan >= 0.0: {}", nan >= 0.0); // both comparisons are false, so an `else` branch is not "the opposite" if nan < 0.0 { println!("negative"); } else { println!("else branch: not negative, but not a number either"); }}0.1 + 0.2 == 0.3: false sum: 0.30000000000000004 close enough: true nan == nan: false nan < 0.0: false nan > 0.0: false nan >= 0.0: false else branch: not negative, but not a number either
Test closeness with a tolerance you choose, not ==, and remember that the else of x < 0.0 is not “x is zero or more” when x might be NaN. Numbers and types covers the rest of float behaviour.
Your language, in Rust
Going deeper
Let chains
An if let can be joined with more tests by &&, and later tests can use names bound earlier. Let chains need edition 2024 (this course uses it): the Rust 1.88 release post says they are available only there.
fn main() { for text in ["42", "-3", "x"] { if let Ok(n) = text.parse::<i32>() && n > 0 { println!("{text:?}: positive number {n}"); } else { println!("{text:?}: not a positive number"); } }}"42": positive number 42 "-3": not a positive number "x": not a positive number
The pieces are joined with &&. A let cannot be joined with ||:
fn main() { let text = "5"; if let Ok(n) = text.parse::<i32>() || text.is_empty() { println!("{n}"); }}error: `||` operators are not supported in let chain conditions --> src/main.rs:3:40 | 3 | if let Ok(n) = text.parse::<i32>() || text.is_empty() { | ^^
A bool as an Option
bool::then_some(value) gives Some(value) when the bool is true and None when it is false. It is close to if c { Some(v) } else { None } (Option), except that then_some evaluates its argument either way. Its sibling then takes a closure and runs it only when true (closures).
fn costly() -> i32 { println!("costly() ran"); 1}
fn main() { let n = 12; // `then_some` turns a bool into an Option: Some(value) when true, None when false println!("{:?}", (n > 10).then_some("big")); println!("{:?}", (n > 20).then_some("huge"));
// the argument is evaluated even when the bool is false let early = (n > 20).then_some(costly()); println!("{:?}", early);
// the same with an if expression let big = if n > 10 { Some("big") } else { None }; println!("{:?}", big);}Some("big")
None
costly() ran
None
Some("big")Practice
Try it
Each exercise has checks in main. Open it in the Playground, replace the todo!(), and run it until it prints all checks passed.
Classify a temperature
Write band (it returns &'static str, a string literal): below 0 is "freezing", 0 to 15 "cold", 16 to 25 "mild", 26 and above "hot". Make one if/else if/else chain the function’s value. The checks include the boundary values and the smallest and largest i32.
// Return the band of a temperature in whole degrees Celsius:// below 0 -> "freezing"// 0 up to 15 -> "cold"// 16 up to 25 -> "mild"// 26 and above -> "hot"// Use an `if` / `else if` chain as the function's value (no `return`).fn band(celsius: i32) -> &'static str { todo!("an if / else if / else chain whose last expression is the answer")}
fn main() { assert_eq!(band(-1), "freezing"); assert_eq!(band(0), "cold"); assert_eq!(band(15), "cold"); assert_eq!(band(16), "mild"); assert_eq!(band(25), "mild"); assert_eq!(band(26), "hot"); assert_eq!(band(i32::MIN), "freezing"); assert_eq!(band(i32::MAX), "hot"); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// Return the band of a temperature in whole degrees Celsius:// below 0 -> "freezing"// 0 up to 15 -> "cold"// 16 up to 25 -> "mild"// 26 and above -> "hot"// Use an `if` / `else if` chain as the function's value (no `return`).fn band(celsius: i32) -> &'static str { if celsius < 0 { "freezing" } else if celsius <= 15 { "cold" } else if celsius <= 25 { "mild" } else { "hot" }}
fn main() { assert_eq!(band(-1), "freezing"); assert_eq!(band(0), "cold"); assert_eq!(band(15), "cold"); assert_eq!(band(16), "mild"); assert_eq!(band(25), "mild"); assert_eq!(band(26), "hot"); assert_eq!(band(i32::MIN), "freezing"); assert_eq!(band(i32::MAX), "hot"); println!("all checks passed");}all checks passed
Validate an age with let else
Write valid_age: text with optional whitespace around a whole number from 0 to 120 gives Some(number), anything else gives None. Use let ... else for the parse and a plain if for the range. The checks include "-5", "121", "4 2", an empty string and a number too big for an i32.
// Turn text typed by a user into an age.// Surrounding spaces are allowed. The age must be a whole number from 0 to 120.// Anything else gives `None`. Use `let ... else` for the parse and a plain `if` for the range.// (`Option` is covered in a later lesson: `Some(n)` is "there is a value", `None` is "there is not".)fn valid_age(text: &str) -> Option<i32> { todo!("parse with let-else and return None on failure, then check 0 to 120")}
fn main() { assert_eq!(valid_age("42"), Some(42)); assert_eq!(valid_age(" 7 "), Some(7)); assert_eq!(valid_age("0"), Some(0)); assert_eq!(valid_age("120"), Some(120)); assert_eq!(valid_age("121"), None); assert_eq!(valid_age("-5"), None); assert_eq!(valid_age("abc"), None); assert_eq!(valid_age("4 2"), None); assert_eq!(valid_age(""), None); assert_eq!(valid_age("99999999999"), None); println!("all checks passed");}Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.
Show a solution
// Turn text typed by a user into an age.// Surrounding spaces are allowed. The age must be a whole number from 0 to 120.// Anything else gives `None`. Use `let ... else` for the parse and a plain `if` for the range.// (`Option` is covered in a later lesson: `Some(n)` is "there is a value", `None` is "there is not".)fn valid_age(text: &str) -> Option<i32> { let Ok(n) = text.trim().parse::<i32>() else { return None; }; if n < 0 || n > 120 { None } else { Some(n) }}
fn main() { assert_eq!(valid_age("42"), Some(42)); assert_eq!(valid_age(" 7 "), Some(7)); assert_eq!(valid_age("0"), Some(0)); assert_eq!(valid_age("120"), Some(120)); assert_eq!(valid_age("121"), None); assert_eq!(valid_age("-5"), None); assert_eq!(valid_age("abc"), None); assert_eq!(valid_age("4 2"), None); assert_eq!(valid_age(""), None); assert_eq!(valid_age("99999999999"), None); println!("all checks passed");}all checks passed
Read more
- The Rust Book, chapter 3.5: Control Flow:
if,else if, andifin alet. - The Rust Book, chapter 6.3: Concise Control Flow with
if letandlet...else - Rust by Example: if/else, if let and let-else
- The Reference:
ifandif letexpressions, including the chains of conditions section. - Standard library:
bool(withthen_some) andf64.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.