if selects a branch from a bool condition.
fn start(): void { println("start") }
fn wait(): void { println("wait") }
fn run(ready: bool): void {
if ready {
start()
} else {
wait()
}
}else is optional. An if without one runs for its effects.
fn f(debug_enabled: bool, state: int): void {
if debug_enabled {
println("state=${state}")
}
}The condition must be a bool
Atoll has no truthiness. Numbers, strings, collections, and optionals are not converted to booleans.
fn f(n: int): int {
if n {
return 1
}
return 0
}fn f(values: []int): int {
if values.len() {
return 1
}
return 0
}fn f(value: int?): int {
if value {
return 1
}
return 0
}Write the comparison you mean:
fn describe(values: []int, value: int?): string {
if values.is_empty() {
return "empty"
}
if value.is_some() {
return "has value"
}
if values.len() > 10 {
return "large"
}
return "small"
}if as an expression
The selected branch’s value becomes the value of the whole if. There is no
ternary operator; a one-line if is the compact form.
fn grade(score: int): string {
label := if score >= 90 {
"excellent"
} else if score >= 60 {
"passing"
} else {
"retry"
}
return label
}
fn sign(value: int): int {
return if value >= 0 { 1 } else { -1 }
}When a value is required, every normally completing branch must produce a compatible type. With an expected type in place, each branch is checked against it individually, so a literal can widen or lift without an intermediate binding:
fn estimate(): f64 { return 0.5 }
fn measure(exact: bool): f64 {
value: f64 = if exact {
1.0
} else {
estimate()
}
return value
}Branches that cannot agree are rejected at the branch, not at the binding:
fn f(use_number: bool): int {
value: int = if use_number {
1
} else {
"two"
}
return value
}Without an expected type the checker joins the branches, which for unrelated types produces an anonymous union you then have to match:
fn render(use_number: bool): string {
value := if use_number {
42
} else {
"forty-two"
}
return match value {
n: int => "number ${n}"
s: string => s
}
}An if used as a statement can leave else off. An if used for its value
generally needs one, because a missing else contributes void.
Chains
else if is an ordered sequence. Conditions are evaluated only until one
matches, and the branches that are not selected have no runtime effect.
fn category(value: int): string {
return if value < 0 {
"negative"
} else if value == 0 {
"zero"
} else if value < 100 {
"small"
} else {
"large"
}
}Reach for match when the decision is about
variants, destructuring, or exhaustively covering a closed type — a long else if chain over one enum loses the coverage check.
Pattern conditions
if pattern := expression runs the first branch only when the expression
matches. The expression is evaluated once, and the pattern’s bindings exist
only inside the successful branch.
struct User { id: int, name: string }
fn lookup(id: int): User? {
if id < 0 { return None }
return Some(User { id: id, name: "user-${id}" })
}
fn greet(id: int): string {
if Some(user) := lookup(id) {
return "hello ${user.name}"
} else {
return "unknown"
}
}The same form works for Result, for enum variants, and for any other pattern:
error ParseError { Invalid }
fn parse(text: string): int ! ParseError {
if text.is_empty() { error Invalid }
return text.len()
}
fn width(text: string): int {
if Ok(value) := parse(text) {
return value
} else if text.is_empty() {
return 0
} else {
return -1
}
}The failure branch cannot read names the pattern never initialized:
fn f(value: int?): int {
if Some(v) := value {
return v
} else {
return v
}
}Bind error payloads as err or e — error is a keyword and is never an
identifier.
Refutable bindings
Pattern := expression else { ... } binds names for the rest of the enclosing
block and puts the failure path in the else.
struct User { id: int, name: string }
fn lookup(id: int): User? {
if id < 0 { return None }
return Some(User { id: id, name: "user-${id}" })
}
fn name_of(id: int): string? {
Some(user) := lookup(id) else {
return None
}
return Some(user.name)
}This is the difference from if pattern := ...: the successful names stay
visible afterwards, so the happy path is not indented. The price is that the
else block must diverge, because control cannot continue with uninitialized
names.
fn f(value: int?): int {
Some(v) := value else {
println("missing")
}
return v
}The compiler answers with let-else's else branch must diverge (return / break / continue / throw / loop). Valid divergence is return, error, and — inside a
loop — break or continue:
error ConfigError { MissingPort }
fn configured_port(): int? { return None }
fn connect_all(hosts: []string): int ! ConfigError {
mut connected := 0
for host in hosts {
Some(port) := configured_port() else {
continue
}
if port <= 0 { error MissingPort }
connected += 1
}
return connected
}Early exits
A branch that always transfers control does not constrain the value type of the branch that completes normally.
error ParseError { Empty }
fn parse(text: string): int ! ParseError {
if text.is_empty() { error Empty }
return text.len()
}
fn width_or_default(text: string): int ! ParseError {
result := if text.is_empty() {
return 0
} else {
parse(text)?
}
return result
}This is the general never-type rule, not a special case for if. The same
holds for error, break, and continue.
Putting it together
error RouteError { NotFound, Rejected { reason: string } }
struct Request { method: string, path: string, token: string }
struct Response { status: int, body: string }
fn authorize(token: string): int? {
if token.is_empty() { return None }
return Some(token.len())
}
fn handle(request: Request): Response ! RouteError {
Some(user) := authorize(request.token) else {
error Rejected { reason: "missing token" }
}
if request.method != "GET" {
error Rejected { reason: "method ${request.method}" }
}
body := if request.path == "/" {
"index"
} else if request.path == "/me" {
"user ${user}"
} else {
error NotFound
}
status := if body.is_empty() { 204 } else { 200 }
return Response { status: status, body: body }
}The refutable binding keeps user in scope for the whole function, the else if
chain picks a body, and the error NotFound branch diverges so it does not have
to produce a string.