Skip to content

The match Control Flow Operator

Rust’s match expression is one of its most powerful features. It allows you to compare a value against a series of patterns and execute code based on which pattern matches. While it might look similar to JavaScript’s switch statement at first glance, it’s far more powerful and flexible.

Basic Pattern Matching

Let’s start with a simple example comparing JavaScript’s switch and Rust’s match:

// JavaScript switch statement
function getStatusMessage(status) {
switch (status) {
case 'PENDING':
return 'Your order is pending';
case 'SHIPPED':
return 'Your order has been shipped';
case 'DELIVERED':
return 'Your order has been delivered';
default:
return 'Unknown status';
}
}
console.log(getStatusMessage('SHIPPED')); // "Your order has been shipped"

The equivalent in Rust:

enum OrderStatus {
Pending,
Shipped,
Delivered,
}
fn get_status_message(status: OrderStatus) -> &'static str {
match status {
OrderStatus::Pending => "Your order is pending",
OrderStatus::Shipped => "Your order has been shipped",
OrderStatus::Delivered => "Your order has been delivered",
}
}
fn main() {
let status = OrderStatus::Shipped;
println!("{}", get_status_message(status)); // "Your order has been shipped"
}

Key differences:

  1. Rust’s match is an expression, not a statement, so it returns a value
  2. There are no explicit return statements or break needed
  3. Each pattern is followed by an expression to evaluate, separated by =>
  4. Rust’s pattern matching works especially well with enums
  5. Rust enforces exhaustiveness - you must handle all possible cases

Matching with Option<T>

One common use of match is with the Option enum:

fn find_user(id: u32) -> Option<User> {
// Implementation details...
}
fn display_user(id: u32) {
match find_user(id) {
Some(user) => println!("Found user: {}", user.name),
None => println!("User not found"),
}
}

The JavaScript equivalent would require null checks:

function findUser(id) {
// Implementation details...
return user || null;
}
function displayUser(id) {
const user = findUser(id);
if (user !== null) {
console.log(`Found user: ${user.name}`);
} else {
console.log("User not found");
}
}

Matching with Result<T, E>

Similarly, match works well with Result:

fn parse_number(input: &str) -> Result<i32, &'static str> {
match input.parse::<i32>() {
Ok(num) => Ok(num),
Err(_) => Err("Invalid number"),
}
}
fn display_parsed_number(input: &str) {
match parse_number(input) {
Ok(num) => println!("Parsed number: {}", num),
Err(e) => println!("Error: {}", e),
}
}

In JavaScript, this would typically use try/catch:

function parseNumber(input) {
const num = parseInt(input, 10);
if (isNaN(num)) {
throw new Error("Invalid number");
}
return num;
}
function displayParsedNumber(input) {
try {
const num = parseNumber(input);
console.log(`Parsed number: ${num}`);
} catch (error) {
console.log(`Error: ${error.message}`);
}
}

Binding Values in Patterns

Rust’s match can extract and bind values from patterns:

enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
enum UsState {
Alabama,
Alaska,
// ... other states
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {:?}!", state);
25
},
}
}

JavaScript would typically use destructuring or property access:

const CoinType = {
PENNY: 'PENNY',
NICKEL: 'NICKEL',
DIME: 'DIME',
QUARTER: 'QUARTER'
};
function valueInCents(coin) {
switch (coin.type) {
case CoinType.PENNY:
return 1;
case CoinType.NICKEL:
return 5;
case CoinType.DIME:
return 10;
case CoinType.QUARTER:
console.log(`State quarter from ${coin.state}!`);
return 25;
default:
throw new Error("Unknown coin");
}
}
const coin = { type: CoinType.QUARTER, state: 'Alaska' };
console.log(valueInCents(coin)); // 25

Matching with Destructuring

Rust’s match can destructure complex data:

struct Point {
x: i32,
y: i32,
}
fn describe_point(point: Point) -> &'static str {
match (point.x, point.y) {
(0, 0) => "at the origin",
(0, _) => "on the y-axis",
(_, 0) => "on the x-axis",
(x, y) if x > 0 && y > 0 => "in the first quadrant",
(x, y) if x < 0 && y > 0 => "in the second quadrant",
(x, y) if x < 0 && y < 0 => "in the third quadrant",
(x, y) if x > 0 && y < 0 => "in the fourth quadrant",
_ => "somewhere else",
}
}

JavaScript would use destructuring and conditions:

function describePoint({ x, y }) {
if (x === 0 && y === 0) return "at the origin";
if (x === 0) return "on the y-axis";
if (y === 0) return "on the x-axis";
if (x > 0 && y > 0) return "in the first quadrant";
if (x < 0 && y > 0) return "in the second quadrant";
if (x < 0 && y < 0) return "in the third quadrant";
if (x > 0 && y < 0) return "in the fourth quadrant";
return "somewhere else";
}

The Catch-All Pattern

The _ pattern matches any value and doesn’t bind it to a variable:

let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}

JavaScript’s equivalent is the default case:

const diceRoll = 9;
switch (diceRoll) {
case 3:
addFancyHat();
break;
case 7:
removeFancyHat();
break;
default:
reroll();
break;
}

Ignoring Values with _

You can use _ to ignore specific parts of a value:

let point = (3, 4);
match point {
(0, 0) => println!("Origin"),
(0, y) => println!("Y-axis at {}", y),
(x, 0) => println!("X-axis at {}", x),
(_, _) => println!("Other point"),
}

Or ignore multiple values:

let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {}, {}, {}", first, third, fifth)
},
}

JavaScript would use destructuring with ignored variables:

const point = [3, 4];
const [x, y] = point;
if (x === 0 && y === 0) {
console.log("Origin");
} else if (x === 0) {
console.log(`Y-axis at ${y}`);
} else if (y === 0) {
console.log(`X-axis at ${x}`);
} else {
console.log("Other point");
}
const numbers = [2, 4, 8, 16, 32];
const [first, _, third, _, fifth] = numbers;
console.log(`Some numbers: ${first}, ${third}, ${fifth}`);

Match Guards with Conditions

You can add if conditions to your patterns:

let num = 4;
match num {
n if n < 0 => println!("Negative number"),
n if n % 2 == 0 => println!("Even number"),
n if n % 2 == 1 => println!("Odd number"),
_ => unreachable!(), // This should never happen
}

JavaScript would use nested conditions:

const num = 4;
if (num < 0) {
console.log("Negative number");
} else if (num % 2 === 0) {
console.log("Even number");
} else if (num % 2 === 1) {
console.log("Odd number");
} else {
// This should never happen
throw new Error("Unreachable");
}

Ranges in Patterns

Rust’s match can match against ranges:

let grade = 85;
match grade {
90..=100 => println!("A"),
80..=89 => println!("B"),
70..=79 => println!("C"),
60..=69 => println!("D"),
_ => println!("F"),
}
let c = 'c';
match c {
'a'..='z' => println!("lowercase letter"),
'A'..='Z' => println!("uppercase letter"),
'0'..='9' => println!("digit"),
_ => println!("something else"),
}

JavaScript would use comparisons:

const grade = 85;
if (grade >= 90 && grade <= 100) {
console.log("A");
} else if (grade >= 80 && grade <= 89) {
console.log("B");
} else if (grade >= 70 && grade <= 79) {
console.log("C");
} else if (grade >= 60 && grade <= 69) {
console.log("D");
} else {
console.log("F");
}
const c = 'c';
if (/[a-z]/.test(c)) {
console.log("lowercase letter");
} else if (/[A-Z]/.test(c)) {
console.log("uppercase letter");
} else if (/[0-9]/.test(c)) {
console.log("digit");
} else {
console.log("something else");
}

Binding with @

You can bind a value to a variable while also testing it against a pattern using @:

enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_var @ 3..=7 } => {
println!("Found an id in range: {}", id_var)
},
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
},
Message::Hello { id } => {
println!("Found some other id: {}", id)
},
}

JavaScript would use a variable and separate test:

const msg = { type: 'Hello', id: 5 };
if (msg.type === 'Hello') {
const id = msg.id;
if (id >= 3 && id <= 7) {
console.log(`Found an id in range: ${id}`);
} else if (id >= 10 && id <= 12) {
console.log("Found an id in another range");
} else {
console.log(`Found some other id: ${id}`);
}
}

Multiple Patterns

You can match against multiple patterns using |:

let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}

JavaScript’s switch can have multiple cases share the same code:

const x = 1;
switch (x) {
case 1:
case 2:
console.log("one or two");
break;
case 3:
console.log("three");
break;
default:
console.log("anything");
break;
}

Matching Structs

Rust’s match works well with structs:

struct Point {
x: i32,
y: i32,
}
let p = Point { x: 0, y: 7 };
match p {
Point { x: 0, y } => println!("On the y-axis at y={}", y),
Point { x, y: 0 } => println!("On the x-axis at x={}", x),
Point { x, y } => println!("At coordinates ({}, {})", x, y),
}

JavaScript would use destructuring:

const p = { x: 0, y: 7 };
const { x, y } = p;
if (x === 0) {
console.log(`On the y-axis at y=${y}`);
} else if (y === 0) {
console.log(`On the x-axis at x=${x}`);
} else {
console.log(`At coordinates (${x}, ${y})`);
}

Nested Patterns

Patterns can be arbitrarily nested:

enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to RGB({}, {}, {})", r, g, b)
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to HSV({}, {}, {})", h, s, v)
}
}

JavaScript would use nested conditionals:

const msg = {
type: 'ChangeColor',
color: { type: 'Hsv', values: [0, 160, 255] }
};
if (msg.type === 'Quit') {
console.log("Quit");
} else if (msg.type === 'Move') {
console.log(`Move to (${msg.x}, ${msg.y})`);
} else if (msg.type === 'Write') {
console.log(`Text message: ${msg.text}`);
} else if (msg.type === 'ChangeColor') {
if (msg.color.type === 'Rgb') {
const [r, g, b] = msg.color.values;
console.log(`Change color to RGB(${r}, ${g}, ${b})`);
} else if (msg.color.type === 'Hsv') {
const [h, s, v] = msg.color.values;
console.log(`Change color to HSV(${h}, ${s}, ${v})`);
}
}

Matching Arrays and Slices

You can match against arrays and slices:

let arr = [1, 2, 3];
match arr {
[1, _, 3] => println!("Array starts with 1 and ends with 3"),
[1, ..] => println!("Array starts with 1"),
[.., 3] => println!("Array ends with 3"),
_ => println!("Some other array"),
}

JavaScript would use array destructuring or methods:

const arr = [1, 2, 3];
if (arr[0] === 1 && arr[2] === 3) {
console.log("Array starts with 1 and ends with 3");
} else if (arr[0] === 1) {
console.log("Array starts with 1");
} else if (arr[arr.length - 1] === 3) {
console.log("Array ends with 3");
} else {
console.log("Some other array");
}

Match Expressions as Values

Since match is an expression, it can be used in variable assignments:

let result = match some_value {
Pattern1 => expression1,
Pattern2 => expression2,
_ => default_expression,
};

JavaScript can use the conditional (ternary) operator for simple cases:

const result = condition1 ? expression1
: condition2 ? expression2
: defaultExpression;

Or a self-executing function for more complex cases:

const result = (() => {
if (condition1) return expression1;
if (condition2) return expression2;
return defaultExpression;
})();

Pattern Matching in Modern JavaScript

Modern JavaScript has improved pattern matching capabilities with destructuring:

// Object destructuring with default values
const { name = 'Guest', age } = user;
// Array destructuring
const [first, second, ...rest] = array;
// Nested destructuring
const { address: { city, country } } = user;

And there’s a proposal for a dedicated pattern matching syntax:

// JavaScript pattern matching proposal (not yet standard)
const result = match (value) {
when { type: 'text', content } -> `Text: ${content}`,
when { type: 'image', url } -> `Image at ${url}`,
when _ -> 'Unknown',
};

But as of now, this is not part of the JavaScript language.

When to Use match

Use match when:

  1. You want to handle all possible cases of an enum
  2. You need to extract and bind variables from a complex structure
  3. You have multiple conditions based on the same value
  4. You want to ensure at compile time that you’ve handled all possible cases

Benefits of match over if/else chains:

  1. Exhaustiveness checking ensures you don’t miss cases
  2. Concise syntax for pattern matching
  3. Clear structure when dealing with complex data
  4. Ability to destructure data in the patterns

Summary

Rust’s match expression is a powerful tool for flow control that:

  1. Compares a value against a series of patterns
  2. Binds parts of the matched value to variables
  3. Ensures all possible cases are handled
  4. Works especially well with enums and complex data structures

While JavaScript has the switch statement and destructuring assignments, they are less powerful than Rust’s pattern matching capabilities. However, the comparison helps JavaScript developers understand how to use match effectively in Rust.

In the next section, we’ll look at if let, a concise way to handle a single pattern match when you don’t need to handle all possible cases.