Skip to content

We are working on this site. Want to help? Open an issue or a pull request on GitHub.

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.

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

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");
  }
}

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}`);
  }
}

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

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 _ 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;
}

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}`);

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");
}

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");
}

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}`);
  }
}

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;
}

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})`);
}

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})`);
  }
}

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");
}

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;
})();

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.

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

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.