Skip to content

Pattern Matching

The match expression lets you branch on the shape of data. The compiler ensures every case is handled.

match status {
"active" -> handleActive(),
"inactive" -> handleInactive(),
_ -> handleUnknown(),
}
match fetchUser(id) {
Ok(user) -> renderProfile(user),
Err(error) -> renderError(error),
}

Both Ok and Err must be handled. Missing a case is a compile error.

match findItem(id) {
Some(item) -> renderItem(item),
None -> renderNotFound(),
}

A variant’s field shape — (...) for positional, { ... } for named — is part of its contract. Pattern matching must use the same shape.

type Shape = | Circle { radius: number }
| Rectangle { width: number, height: number }
| Triangle { base: number, height: number }
let area(shape: Shape) -> number = {
match shape {
Circle { radius } -> 3.14159 * radius * radius,
Rectangle { width, height } -> width * height,
Triangle { base, height: h } -> 0.5 * base * h,
}
}

Inside a named pattern, { width } is shorthand for { width: width }. Use { width: w } to rebind under a different name.

type Shape = | Circle(number)
| Rectangle(number, number)
| Triangle(number, number)
let area(shape: Shape) -> number = {
match shape {
Circle(r) -> 3.14159 * r * r,
Rectangle(w, h) -> w * h,
Triangle(b, h) -> 0.5 * b * h,
}
}

Pick the shape based on whether the fields carry meaningful names. Using the wrong pattern shape — Circle { radius: r } on a positional variant, or Rectangle(w, h) on a named variant — is a compile error.

Adding a new variant to Shape without updating the match is also a compile error.

match score {
0..59 -> "F",
60..69 -> "D",
70..79 -> "C",
80..89 -> "B",
90..100 -> "A",
_ -> "Invalid",
}
match event {
{ kind: "click", x, y } -> handleClick(x, y),
{ kind: "keydown", key } -> handleKey(key),
_ -> ignore(),
}
match result {
Ok(Some(value)) -> process(value),
Ok(None) -> useDefault(),
Err(e) -> handleError(e),
}

Match strings with {name} captures to extract parts:

let route(url: string) -> Page = {
match url {
"/users/{id}" -> fetchUser(id),
"/users/{id}/posts/{postId}" -> fetchPost(id, postId),
"/about" -> aboutPage(),
_ -> notFound(),
}
}

Captured variables (id, postId) are bound as string in the match arm body. The pattern compiles to regex matching with capture groups.

This is useful for URL routing, string parsing, and any case where you need to extract structured data from strings.

Match on array structure with head/tail destructuring:

match items {
[] -> "empty",
[only] -> `just one: ${only}`,
[first, second] -> "exactly two",
[first, ..rest] -> `first is ${first}, rest has ${rest |> Array.length}`,
}
PatternMatchesBinds
[]Empty arrayNothing
[a]Exactly 1 elementa
[a, b]Exactly 2 elementsa, b
[first, ..rest]1 or more elementsfirst (head), rest (tail array)
[first, second, ..rest]2 or more elementsfirst, second, rest
[_, ..rest]1 or more, ignore headrest

[] combined with [_, ..rest] covers all cases (empty + non-empty):

match items {
[] -> "nothing here",
[head, ..tail] -> `starts with ${head}`,
}

A pattern like [a] alone is NOT exhaustive - it only matches arrays of exactly one element.

The _ pattern matches anything. Place it last as a default:

match value {
1 -> "one",
2 -> "two",
_ -> "other",
}

Use when to add a condition to a match arm. The arm only matches if both the pattern matches and the guard expression is true.

match user {
User(age) when age >= 65 -> "senior",
User(age) when age >= 18 -> "adult",
User(age) -> "minor",
}

Bindings from the pattern are available in the guard expression.

Guards work with any pattern, including wildcards:

match score {
_ when score > 90 -> "excellent",
_ when score > 70 -> "good",
_ -> "needs improvement",
}

A guarded arm does not count as exhaustive coverage for its pattern. You still need an unguarded catch-all or complete coverage:

match value {
Ok(x) when x > 0 -> handle(x),
// This alone is NOT exhaustive - the guard might fail.
// Add unguarded arms:
Ok(x) -> handleNegative(x),
Err(e) -> handleError(e),
}

You can pipe a value directly into match to combine pipelines with pattern matching:

let label = price |> match {
_ when _ < 10 -> "cheap",
_ when _ < 100 -> "moderate",
_ -> "expensive",
}

This works at the end of a pipeline chain:

let label = product
|> effectivePrice
|> match {
_ when _ < 10 -> "cheap",
_ when _ < 100 -> "moderate",
_ -> "expensive",
}

x |> match { ... } is pure syntax sugar and compiles identically to match x { ... }. See Pipes for more details.

The compiler checks that your match is exhaustive:

// Compile error: non-exhaustive match on boolean
match enabled {
true -> "on",
// missing: false
}

This applies to:

  • Booleans - must handle true and false
  • Result - must handle Ok and Err
  • Option - must handle Some and None
  • Unions - must handle every variant (or use _)
  • Arrays - [] + [_, ..rest] covers all cases (empty + non-empty)