learn.sol
Rust Foundations • Enums And Pattern Matching

Enums and Pattern Matching in Rust

Learn how Rust enums model mutually exclusive states and how match, if let, Option, and Result help you handle those states explicitly.

People often describe enums as "a value with a few named options."

That is technically true, but it is too weak to be useful.

The better mental model is this:

An enum lets one value be in exactly one valid state at a time, and pattern matching forces you to handle that state explicitly.

That is why enums matter so much in Rust.

They are not just cleaner constants.

They are how Rust models situations where different states carry different data and require different behavior.

Enums become much more useful once you stop thinking of them as labels and start thinking of them as state shapes.

Why Enums Exist

A struct is good when every instance has the same shape.

For example, every Wallet might have:

  • an owner
  • a balance
  • a frozen flag

That is one consistent layout.

But some problems do not have one consistent layout.

A transaction status might be:

  • pending with no extra data yet
  • confirmed with a slot and signature
  • failed with an error message

Those states do not all need the same fields.

If you try to force them into one struct, you usually end up with weak designs like:

struct TransactionStatus {
    is_pending: bool,
    confirmed_slot: Option<u64>,
    signature: Option<String>,
    error: Option<String>,
}

This compiles, but the shape is vague.

It allows nonsense combinations like:

  • is_pending = true and signature = Some(...)
  • confirmed_slot = Some(...) and error = Some(...)

The type does not protect the meaning.

An enum does.

Your First Useful Enum

#[derive(Debug)]
enum TransactionStatus {
    Pending,
    Confirmed { slot: u64, signature: String },
    Failed { error: String },
}

This is stronger because a value can only be one of these states:

  • Pending
  • Confirmed { ... }
  • Failed { ... }

Not two at once.

Not half-filled.

Not ambiguous.

That is the main benefit.

How Variant Data Works

Enums can store different kinds of variants.

#[derive(Debug)]
enum WalletAction {
    Deposit(u64),
    Withdraw(u64),
    Freeze,
    Rename { new_owner: String },
}

This one enum contains four different shapes:

  • Deposit(u64) stores one amount
  • Withdraw(u64) stores one amount
  • Freeze stores no extra data
  • Rename { new_owner: String } stores named data

Rust lets you do this because the enum value is always exactly one variant at runtime.

Pattern Matching Is The Other Half

Enums are only half the story.

Pattern matching is what makes them useful.

When you use match, you tell Rust:

  • inspect the current variant
  • unpack any data inside it
  • run the correct branch
#[derive(Debug)]
enum WalletAction {
    Deposit(u64),
    Withdraw(u64),
    Freeze,
    Rename { new_owner: String },
}

fn describe_action(action: WalletAction) {
    match action {
        WalletAction::Deposit(amount) => {
            println!("deposit {} lamports", amount);
        }
        WalletAction::Withdraw(amount) => {
            println!("withdraw {} lamports", amount);
        }
        WalletAction::Freeze => {
            println!("freeze wallet");
        }
        WalletAction::Rename { new_owner } => {
            println!("rename wallet to {}", new_owner);
        }
    }
}

Why match Matters

match is not just nicer syntax.

It gives you exhaustiveness.

That means Rust checks that you handled every variant.

If you later add this variant:

enum WalletAction {
    Deposit(u64),
    Withdraw(u64),
    Freeze,
    Rename { new_owner: String },
    Close,
}

your old match stops compiling until you handle Close too.

That is one of the biggest reasons Rust code stays honest as it grows.

The compiler forces your control flow to stay in sync with your data model.

A Small End-To-End Example

Now put the pieces together.

#[derive(Debug)]
enum TransactionStatus {
    Pending,
    Confirmed { slot: u64, signature: String },
    Failed { error: String },
}

fn print_status(status: &TransactionStatus) {
    match status {
        TransactionStatus::Pending => {
            println!("transaction is still pending");
        }
        TransactionStatus::Confirmed { slot, signature } => {
            println!("confirmed in slot {} with signature {}", slot, signature);
        }
        TransactionStatus::Failed { error } => {
            println!("transaction failed: {}", error);
        }
    }
}

fn main() {
    let pending = TransactionStatus::Pending;
    let confirmed = TransactionStatus::Confirmed {
        slot: 245,
        signature: String::from("5abc...xyz"),
    };
    let failed = TransactionStatus::Failed {
        error: String::from("insufficient funds"),
    };

    print_status(&pending);
    print_status(&confirmed);
    print_status(&failed);
}

What this code is proving

  • one type can represent multiple valid runtime states
  • different states can carry different data
  • match can both inspect and unpack that data
  • each branch can stay specific to the current state

That is the core pattern you will keep using.

Option<T> Is Just An Enum

People often learn Option<T> as a special feature.

It is not special.

It is a normal enum from the standard library.

enum Option<T> {
    Some(T),
    None,
}

That means every time you use Option, you are already using enums and pattern matching.

fn find_balance(name: &str) -> Option<u64> {
    match name {
        "alice" => Some(1_500_000),
        "bob" => Some(800_000),
        _ => None,
    }
}

fn main() {
    let balance = find_balance("alice");

    match balance {
        Some(amount) => println!("balance: {}", amount),
        None => println!("wallet not found"),
    }
}

Why Option matters

Without Option, many languages use:

  • null
  • sentinel values
  • vague booleans

Rust makes absence explicit in the type.

You cannot quietly ignore it.

Result<T, E> Is Also Just An Enum

Result works the same way.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Use it when an operation can:

  • succeed with a value
  • fail with an error
fn withdraw(balance: u64, amount: u64) -> Result<u64, String> {
    if amount > balance {
        return Err(String::from("insufficient balance"));
    }

    Ok(balance - amount)
}

fn main() {
    let result = withdraw(500, 200);

    match result {
        Ok(new_balance) => println!("new balance: {}", new_balance),
        Err(message) => println!("withdraw failed: {}", message),
    }
}

Why Result matters

This is much stronger than returning:

  • a magic value like -1
  • a success boolean plus separate output state
  • a string that might or might not mean failure

The type tells you the operation can fail.

The caller has to deal with that honestly.

When To Use if let

Sometimes a full match is more than you need.

If you only care about one pattern, if let is often cleaner.

fn main() {
    let status = TransactionStatus::Confirmed {
        slot: 245,
        signature: String::from("5abc...xyz"),
    };

    if let TransactionStatus::Confirmed { slot, signature } = status {
        println!("confirmed in slot {} with signature {}", slot, signature);
    }
}

Use if let when:

  • you only care about one successful pattern
  • the other cases do not need detailed handling

Use match when:

  • every variant matters
  • you want the compiler to enforce complete handling
  • each branch has meaningful logic

A More Realistic Example

This example looks closer to what you will build in practice pages and future Solana code.

#[derive(Debug)]
enum AccountType {
    System,
    Token { mint: String },
    Program { executable: bool },
}

fn describe_account(account_type: &AccountType) -> String {
    match account_type {
        AccountType::System => String::from("system account"),
        AccountType::Token { mint } => {
            format!("token account for mint {}", mint)
        }
        AccountType::Program { executable } => {
            if *executable {
                String::from("executable program account")
            } else {
                String::from("program-owned data account")
            }
        }
    }
}

This is the real strength of enums.

They let you model meaning directly instead of hiding it in scattered booleans and optional fields.

Common Mistakes

Mistake 1: Using enums like simple labels

If all variants are just names and none of them ever carry meaningful data, ask whether the enum is actually modeling state or just replacing strings.

Sometimes that is still fine.

Often it means the model is still too shallow.

Mistake 2: Avoiding match with weak shortcuts

If every branch matters, do not reach for nested if statements just because they feel familiar.

match is clearer and safer.

Mistake 3: Fighting the type system with duplicated flags

This is weak:

struct Status {
    pending: bool,
    failed: bool,
    error: Option<String>,
}

This is stronger:

enum Status {
    Pending,
    Confirmed,
    Failed { error: String },
}

The enum prevents invalid combinations.

Mistake 4: Using if let when full handling matters

if let is convenient, but it can hide ignored states.

If the other variants matter to correctness, use match.

Practice

Before moving on, make sure you can do these without guessing:

  1. Write an enum where one variant stores no data, one stores a tuple value, and one stores named fields.
  2. Write a match that handles every variant and prints different output for each one.
  3. Explain why Option<T> and Result<T, E> are enums, not special exceptions to the rule.
  4. Explain why an enum is often better than a struct full of booleans and Option fields.

What You Should Know Now

After this lesson, you should be able to say:

  • an enum models one of several valid states
  • each variant can carry its own data shape
  • match is how you inspect and unpack those states safely
  • Option and Result are standard-library enums built on the same idea

That is the foundation you need for the next page.

The practice lesson will push this further by making you model accounts, transaction states, and wallet operations with enums instead of vague structs.

Solana Assistant

AI-powered documentation helper

Welcome to Solana Assistant

Ask specific questions about Solana development:

Ask specific questions for better results400px
    Enums and Pattern Matching in Rust | learn.sol