Rust Enums: Option, Result & Pattern Matching Guide

Max WellsMax WellsFounder of Rustify

TL;DR: Rust enums are algebraic data types where each variant can optionally hold different types of data. Unlike enums in C, Java, or TypeScript (which are just named integers or string unions), Rust enums can carry payloads. This makes them ideal for modeling states, errors, and optional values. Option<T> and Result<T, E>; the two most important types in Rust; are both enums.


What Is an Enum in Rust?

A Rust enum defines a type that can be one of several variants, where each variant can optionally hold different data; making enums the primary tool for modeling sum types (also called tagged unions or discriminated unions).

enum Shape {
    Circle(f64),           // variant with one value (radius)
    Rectangle(f64, f64),   // variant with two values (width, height)
    Triangle { base: f64, height: f64 }, // variant with named fields
    Point,                 // variant with no data
}

Each Shape value is exactly one of these variants; the enum guarantees exhaustive representation of all possibilities.


How Do You Use Enums With Pattern Matching?

Enums are used with match expressions that destructure each variant; the compiler requires you to handle every variant, preventing missed cases.

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h) => w * h,
        Shape::Triangle { base, height } => 0.5 * base * height,
        Shape::Point => 0.0,
    }
}
 
fn main() {
    let c = Shape::Circle(5.0);
    println!("Area: {:.2}", area(&c)); // Area: 78.54
}

If you add a new variant to Shape without updating the match, the compiler produces an error; impossible to forget a case.


What Is Option<T>?

Option<T> is Rust's built-in enum for representing values that may or may not be present; replacing null pointers from other languages.

enum Option<T> {
    Some(T),  // value is present
    None,     // value is absent
}
fn find_user(id: u64) -> Option<String> {
    if id == 1 {
        Some("Alice".to_string())
    } else {
        None
    }
}
 
fn main() {
    match find_user(1) {
        Some(name) => println!("Found: {name}"),
        None => println!("User not found"),
    }
 
    // Shorthand methods:
    let name = find_user(1).unwrap_or("Unknown".to_string());
    let upper = find_user(1).map(|n| n.to_uppercase());
}

Option eliminates null pointer exceptions; Rust has no null. Every potentially absent value is explicit in the type system.


What Is Result<T, E>?

Result<T, E> is Rust's built-in enum for operations that can succeed or fail; the foundation of error handling in Rust.

enum Result<T, E> {
    Ok(T),   // success with value
    Err(E),  // failure with error
}
use std::num::ParseIntError;
 
fn parse_port(s: &str) -> Result<u16, ParseIntError> {
    let n: u16 = s.parse()?; // ? propagates errors automatically
    Ok(n)
}
 
fn main() {
    match parse_port("8080") {
        Ok(port) => println!("Listening on port {port}"),
        Err(e) => println!("Invalid port: {e}"),
    }
}

The ? operator is syntactic sugar for early return on Err; it makes error propagation concise without hiding failures.


How Are Enums Different From Other Languages?

In most languages, enums are named constants (Java, C) or string/number unions (TypeScript). Rust enums are algebraic data types; each variant is a distinct type that can carry arbitrary data.

FeatureC enumJava enumTypeScript unionRust enum
Named variants
Variants hold dataLimited
Pattern matchingPartial✅ Exhaustive
Compiler enforces all casesPartial
Used for error handling✅ (Result)

Frequently Asked Questions

Yes. Use impl EnumName { } the same way you would for structs. Enums can have methods, associated functions, and implement traits.

if let is shorthand for match when you only care about one variant: if let Some(x) = optional { use(x) }. Use it when handling a single variant is all you need; use match when you need to handle multiple variants.

Yes. #[derive(Debug, Clone, PartialEq)] works on enums the same as structs. #[derive(serde::Serialize, serde::Deserialize)] makes enums serializable to JSON, TOML, etc.

Marking an enum #[non_exhaustive] tells downstream crates that new variants may be added in future versions. This forces them to include a wildcard _ arm in match expressions, preventing breakage when variants are added.


Sources


  • Option: Rust's null-safety enum
  • Result: Rust's error handling enum
  • Trait: Enums can implement traits
  • if let: if let is one of the most common ways to match a single enum variant
  • Pattern Matching: Enums are the core data type that makes Rust pattern matching powerful

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?