| |

Rust 22 🦀 Enums Definition and Encoding Data in Variants

A struct says “this is a thing with these fields.” An enum says “this is one of several possible things.” This distinction is fundamental, and Rust’s enums take it further than most languages. In Rust, each variant of an enum can carry its own data—different types, different amounts, different shapes. An enum is not just a list of named constants. It is a type that encodes the idea that a value is exactly one of a set of alternatives, and each alternative can hold whatever data makes sense for it.

This chapter covers enum definition and the data-carrying variants that make Rust’s enums so expressive. You will learn the syntax for unit, tuple, and struct variants, how to match on enums with match and if let, how the standard library’s Option and Result use enums to encode absence and failure, and how enums interact with methods, generics, and pattern matching. You will also see the patterns that replace class hierarchies in object-oriented languages.

Key point: An enum value is exactly one of its variants. Each variant can hold different data. The compiler tracks which variant a value is at compile time, and pattern matching is how you extract the data from a variant. You cannot access a variant’s data without first checking which variant the value is.


Why Rust enums exist

The alternatives problem. Real programs deal with values that can be one of several things. A network message is a connect, disconnect, or data packet. A shape is a circle, rectangle, or triangle. A JSON value is a string, number, boolean, array, object, or null. The natural way to model these is a type that can be exactly one of a fixed set of alternatives. Rust’s enums express this directly, and the compiler enforces that every possible variant is handled.

The data-carrying problem. In many languages, enums are just named integers. They cannot carry data. If you need a “shape” that is either a circle (with a radius) or a rectangle (with width and height), you need a separate class hierarchy or a tagged union with manual bookkeeping. Rust’s enums carry data per variant, so a Circle variant can hold an f64 radius while a Rectangle variant holds two f64 fields. The data is part of the variant, and pattern matching extracts it safely.

The null problem. Tony Hoare called null his “billion-dollar mistake.” Rust has no null. Instead, it has Option<T>, an enum with two variants: Some(T) and None. A value that might be absent is not a nullable reference; it is an Option. The compiler forces you to handle both cases. This is only possible because enums can carry data.

The error-handling problem. Rust has no exceptions. Functions that can fail return Result<T, E>, an enum with Ok(T) and Err(E) variants. The caller must handle both. This makes error paths explicit in the type system, and it is built entirely on the enum mechanism.

The exhaustive matching problem. When you match on an enum, the compiler verifies that you have handled every variant. If you add a new variant later, every match that does not handle it becomes a compile error. This is a powerful refactoring aid: the compiler tells you exactly where the new variant needs to be considered.


a. Defining enums: unit, tuple, and struct variants

The simplest enum has unit variants—variants that carry no data. These are like named constants in other languages.

enum Direction {
    North,
    South,
    East,
    West,
}

A value of type Direction is one of these four variants. You create one with Direction::North and match on it with match.

Tuple variants carry data of specified types. The data is accessed by position, like a tuple.

enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle(f64, f64, f64),
}

A Shape::Circle(3.0) holds a single f64. A Shape::Rectangle(4.0, 5.0) holds two. A Shape::Triangle(3.0, 4.0, 5.0) holds three. Each variant has a different arity and different types. The compiler knows which is which.

Struct variants carry named fields. This is useful when the data has meaning that names clarify.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

Message::Quit is a unit variant. Message::Move { x: 10, y: 20 } is a struct variant with named fields. Message::Write(String::from("hello")) is a tuple variant. Message::ChangeColor(255, 0, 0) is a tuple variant with three values. All four coexist in one enum, and the compiler tracks which is which.

The match expression is how you extract the data:

fn process(msg: Message) {
    match msg {
        Message::Quit => println!("Quitting"),
        Message::Move { x, y } => println!("Moving to ({}, {})", x, y),
        Message::Write(text) => println!("Writing: {}", text),
        Message::ChangeColor(r, g, b) => println!("Color: ({}, {}, {})", r, g, b),
    }
}

Each arm destructures the variant and binds its data to local variables. The match is exhaustive—every variant is handled. If one is missing, the compiler rejects the code.

Enums can have methods through impl blocks:

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => std::f64::consts::PI * r * r,
            Shape::Rectangle(w, h) => w * h,
            Shape::Triangle(a, b, c) => {
                let s = (a + b + c) / 2.0;
                (s * (s - a) * (s - b) * (s - c)).sqrt()
            }
        }
    }
}

The method takes &self and matches on it. This is how enums get behavior: the behavior is defined once, and the match dispatches on the variant. This replaces the virtual method dispatch of object-oriented languages with compile-time pattern matching.


b. Option and Result: enums in the standard library

The two most important enums in Rust are Option<T> and Result<T, E>. They are defined in the standard library and used everywhere.

Option<T> encodes the presence or absence of a value:

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

A function that might not find something returns Option<T>. The caller must handle both cases:

fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Alice"))
    } else {
        None
    }
}

match find_user(1) {
    Some(name) => println!("Found: {}", name),
    None => println!("Not found"),
}

There is no null in Rust. Absence is encoded in the type, and the compiler forces you to acknowledge it. You cannot call a method on the String inside Option<String> without first extracting it—either with match, with if let, or with combinators like map, unwrap_or, or ?.

Result<T, E> encodes success or failure:

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

A function that can fail returns Result. The caller must handle both the success and the error:

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(a / b)
    }
}

match divide(10.0, 2.0) {
    Ok(result) => println!("Result: {}", result),
    Err(e) => println!("Error: {}", e),
}

The ? operator propagates errors automatically. It works on Result and Option:

fn read_number() -> Result<i32, std::num::ParseIntError> {
    let mut input = String::new();
    std::io::stdin().read_line(&mut input)?;
    let n: i32 = input.trim().parse()?;
    Ok(n)
}

If read_line or parse returns an Err, the ? operator returns it from the enclosing function. If both succeed, the function continues. This is Rust’s replacement for exceptions, and it is built entirely on Result.

Both Option and Result have many combinator methods—map, and_then, unwrap_or, ok_or, unwrap_or_else—that let you chain operations without explicit match blocks. These methods are defined in impl blocks in the standard library, which is possible because they are just methods on enums.


c. Matching, if let, and refactoring with enums

Pattern matching is the primary way to work with enums. The match expression is exhaustive: it must handle every variant, or the compiler rejects it.

fn describe(value: Option<i32>) -> String {
    match value {
        Some(n) if n > 0 => format!("positive: {}", n),
        Some(n) if n < 0 => format!("negative: {}", n),
        Some(0) => String::from("zero"),
        None => String::from("nothing"),
    }
}

Match guards (if n > 0) add conditions to arms. The wildcard _ can catch all remaining cases when you do not need to name them, but using _ defeats the exhaustiveness check for refactoring purposes.

The if let construct is a shorthand for matching one pattern:

if let Some(name) = find_user(1) {
    println!("Found: {}", name);
}

This is equivalent to a match with one arm and a _ => {} catch-all. It is useful when you only care about one variant. The let else construct, stabilized in Rust 1.65, handles the inverse:

let Some(name) = find_user(1) else {
    println!("Not found");
    return;
};
println!("Found: {}", name);

The let else extracts the value or runs the else block, which must diverge (return, panic, or continue). This keeps the happy path unindented.

Enums are the primary tool for replacing class hierarchies in Rust. An object-oriented design with a base class and subclasses becomes an enum with variants. A visitor pattern becomes a match. This is sometimes called “enum dispatch” or “data-oriented design.”

enum Expr {
    Number(f64),
    Add(Box<Expr>, Box<Expr>),
    Multiply(Box<Expr>, Box<Expr>),
}

impl Expr {
    fn eval(&self) -> f64 {
        match self {
            Expr::Number(n) => *n,
            Expr::Add(a, b) => a.eval() + b.eval(),
            Expr::Multiply(a, b) => a.eval() * b.eval(),
        }
    }
}

The Box<Expr> is necessary because the enum is recursive. Without the box, the size of Expr would be infinite. This pattern—recursive enums with boxed children—is the standard way to build expression trees, ASTs, and other recursive structures in Rust.

Adding a new variant is a breaking change in a good way: the compiler forces every match to handle it. This is the opposite of adding a method to a base class in an OO language, where subclasses may silently inherit a default that does not apply.


Complete Example Session

// ============================================
// PART 1: UNIT VARIANTS
// ============================================
enum Direction {
    North,
    South,
    East,
    West,
}

fn turn_right(d: Direction) -> Direction {
    match d {
        Direction::North => Direction::East,
        Direction::East => Direction::South,
        Direction::South => Direction::West,
        Direction::West => Direction::North,
    }
}
// ============================================
// PART 2: TUPLE VARIANTS
// ============================================
enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
    Triangle(f64, f64, f64),
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(r) => std::f64::consts::PI * r * r,
        Shape::Rectangle(w, h) => w * h,
        Shape::Triangle(a, b, c) => {
            let s = (a + b + c) / 2.0;
            (s * (s - a) * (s - b) * (s - c)).sqrt()
        }
    }
}
// ============================================
// PART 3: STRUCT VARIANTS
// ============================================
enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(u8, u8, u8),
}

fn process(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Write: {}", text),
        Message::ChangeColor(r, g, b) => println!("Color ({}, {}, {})", r, g, b),
    }
}
// ============================================
// PART 4: METHODS ON ENUMS
// ============================================
impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle(r) => std::f64::consts::PI * r * r,
            Shape::Rectangle(w, h) => w * h,
            Shape::Triangle(a, b, c) => {
                let s = (a + b + c) / 2.0;
                (s * (s - a) * (s - b) * (s - c)).sqrt()
            }
        }
    }
}
// ============================================
// PART 5: OPTION
// ============================================
fn find_first_even(numbers: &[i32]) -> Option<i32> {
    for &n in numbers {
        if n % 2 == 0 {
            return Some(n);
        }
    }
    None
}

fn main() {
    match find_first_even(&[1, 3, 5]) {
        Some(n) => println!("Found: {}", n),
        None => println!("No even number"),
    }
}
// ============================================
// PART 6: RESULT
// ============================================
fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err(String::from("Division by zero"))
    } else {
        Ok(a / b)
    }
}

fn main() {
    match divide(10.0, 0.0) {
        Ok(result) => println!("Result: {}", result),
        Err(e) => println!("Error: {}", e),
    }
}
// ============================================
// PART 7: IF LET
// ============================================
fn main() {
    let numbers = vec![1, 2, 3, 4];
    if let Some(n) = find_first_even(&numbers) {
        println!("Found: {}", n);
    }
}
// ============================================
// PART 8: LET ELSE
// ============================================
fn process_input(input: Option<i32>) -> i32 {
    let Some(n) = input else {
        return 0;
    };
    n * 2
}
// ============================================
// PART 9: RECURSIVE ENUM
// ============================================
enum Expr {
    Number(f64),
    Add(Box<Expr>, Box<Expr>),
    Multiply(Box<Expr>, Box<Expr>),
}

impl Expr {
    fn eval(&self) -> f64 {
        match self {
            Expr::Number(n) => *n,
            Expr::Add(a, b) => a.eval() + b.eval(),
            Expr::Multiply(a, b) => a.eval() * b.eval(),
        }
    }
}
// ============================================
// PART 10: MATCH WITH GUARDS AND BINDING
// ============================================
fn describe(n: Option<i32>) -> String {
    match n {
        Some(x) if x > 0 => format!("positive: {}", x),
        Some(x) if x < 0 => format!("negative: {}", x),
        Some(0) => String::from("zero"),
        None => String::from("nothing"),
    }
}

The ten parts covered unit variants, tuple variants, struct variants, methods on enums, Option, Result, if let, let else, recursive enums, and match guards.


Quick Reference

Enum Variant Forms

FormSyntaxExample
UnitNameDirection::North
TupleName(T1, T2)Shape::Rectangle(f64, f64)
StructName { field: T }Message::Move { x: i32, y: i32 }
RecursiveName(Box<Self>)Expr::Add(Box<Expr>, Box<Expr>)

Creating and Matching

OperationSyntax
Create unitDirection::North
Create tupleShape::Circle(3.0)
Create structMessage::Move { x: 1, y: 2 }
Matchmatch value { ... }
Match oneif let Some(x) = value { }
Extract or divergelet Some(x) = value else { }

Standard Enums

EnumVariantsPurpose
Option<T>Some(T), NonePresence or absence
Result<T, E>Ok(T), Err(E)Success or failure
OrderingLess, Equal, GreaterComparison result
Cow<'a, B>Borrowed, OwnedClone-on-write

Match Patterns

PatternMatches
VariantUnit variant
Variant(x)Tuple variant
Variant { field }Struct variant
Variant(_)Variant ignoring data
_Any value
Some(x) if x > 0Guard condition
x @ 1..=5Binding with range

Best Practices

✅ Do This:

// Use enums for mutually exclusive alternatives
enum Status { Active, Inactive, Pending }                      // ✅

// Use tuple variants for unnamed data
enum Shape { Circle(f64), Rectangle(f64, f64) }                // ✅

// Use struct variants for named fields
enum Event { Click { x: i32, y: i32 } }                        // ✅

// Use Option for absence
fn find(id: u32) -> Option<User> { }                           // ✅

// Use Result for fallible operations
fn parse(s: &str) -> Result<i32, ParseIntError> { }            // ✅

// Use let else for early returns
let Some(x) = value else { return; };                          // ✅

// Box recursive variants
enum List { Cons(i32, Box<List>), Nil }                        // ✅

❌ Don’t Do This:

// Don't use string or int constants instead of enums
const STATUS_ACTIVE: i32 = 1;                                  // ❌

// Don't forget to handle all variants
match direction {
    Direction::North => { }
}                                                              // ❌ not exhaustive

// Don't use Option when a value is always present
fn get_name() -> Option<String> { Some("x".into()) }           // ⚠️

// Don't panic on Result when you can propagate
let n = parse(s).unwrap();                                     // ⚠️ use ?

// Don't forget Box for recursive enums
enum Expr { Add(Expr, Expr) }                                  // ❌ infinite size

// Don't use wildcard to silence exhaustiveness
match value {
    _ => { }
}                                                              // ⚠️ hides new variants

Common Pitfalls

PitfallWhy It HappensFix
Non-exhaustive matchMissing variantAdd the missing arm
Infinite sizeRecursive enum without BoxWrap recursive field in Box
Cannot access variant dataDid not match firstUse match, if let, or let else
Move error in matchMatching by value consumesMatch on &value or use ref
unwrap panicNone or Err encounteredUse ? or handle explicitly
Unreachable patternEarlier arm catches allReorder arms
Type mismatch in variantWrong data typeCheck variant signature

Real-World Examples

1. State Machine

enum State {
    Idle,
    Running { pid: u32 },
    Stopped { exit_code: i32 },
}

2. JSON Value

enum Json {
    Null,
    Bool(bool),
    Number(f64),
    String(String),
    Array(Vec<Json>),
    Object(HashMap<String, Json>),
}

3. AST Node

enum Node {
    Literal(i64),
    Binary { op: Op, left: Box<Node>, right: Box<Node> },
}

4. Command

enum Command {
    Quit,
    Echo(String),
    Move { x: i32, y: i32 },
}

5. API Response

enum Response {
    Success { data: String },
    Error { code: u16, message: String },
    Redirect { url: String },
}

6. Config Value

enum ConfigValue {
    String(String),
    Integer(i64),
    Boolean(bool),
    List(Vec<ConfigValue>),
}

7. User Input

enum Input {
    Key(char),
    Mouse { x: i32, y: i32 },
    Resize(u32, u32),
}

8. Result Wrapper

enum Outcome<T> {
    Success(T),
    Partial(T, Vec<String>),
    Failure(Vec<String>),
}

9. Token Type

enum Token {
    Identifier(String),
    Number(f64),
    Operator(char),
    Eof,
}

10. Either Type

enum Either<L, R> {
    Left(L),
    Right(R),
}

Visual

Enum with Data-Carrying Variants

┌─────────────────────────────────────────────────────────────┐
│  enum Message                                               │
│                                                             │
│  ┌───────────────────────────────────────────────────┐      │
│  │                                                   │      │
│  │  Quit                    ── unit variant           │      │
│  │  (no data)                                        │      │
│  │                                                   │      │
│  │  Move { x: i32, y: i32 } ── struct variant         │      │
│  │  (named fields)                                   │      │
│  │                                                   │      │
│  │  Write(String)           ── tuple variant          │      │
│  │  (one unnamed value)                              │      │
│  │                                                   │      │
│  │  ChangeColor(u8,u8,u8)   ── tuple variant          │      │
│  │  (three unnamed values)                           │      │
│  │                                                   │      │
│  └───────────────────────────────────────────────────┘      │
│                                                             │
│  A Message value is exactly one of these.                   │
│  Each variant carries different data.                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Option and Result

┌─────────────────────────────────────────────────────────────┐
│  Option<T>                                                  │
│                                                             │
│  ┌─────────────────┐  ┌─────────────────┐                   │
│  │  Some(value)    │  │  None           │                   │
│  │  (has a T)      │  │  (no value)     │                   │
│  └─────────────────┘  └─────────────────┘                   │
│                                                             │
│  Replaces null. Compiler forces handling both.              │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Result<T, E>                                               │
│                                                             │
│  ┌─────────────────┐  ┌─────────────────┐                   │
│  │  Ok(value)      │  │  Err(error)     │                   │
│  │  (success)      │  │  (failure)      │                   │
│  └─────────────────┘  └─────────────────┘                   │
│                                                             │
│  Replaces exceptions. ? operator propagates errors.         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Match Exhaustiveness

┌─────────────────────────────────────────────────────────────┐
│  MATCH MUST HANDLE ALL VARIANTS                             │
│                                                             │
│  enum Direction { North, South, East, West }                │
│                                                             │
│  match d {                                                  │
│    Direction::North => { }                                  │
│    Direction::South => { }                                  │
│    Direction::East  => { }                                  │
│    Direction::West  => { }                                  │
│  }                                                          │
│  ✓ Compiles: all four variants handled                      │
│                                                             │
│  match d {                                                  │
│    Direction::North => { }                                  │
│    Direction::South => { }                                  │
│  }                                                          │
│  ✗ Error: missing East and West                             │
│                                                             │
│  Adding a new variant breaks all incomplete matches.        │
│  The compiler tells you where to update.                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Enum vs Struct vs Class Hierarchy

┌─────────────────────────────────────────────────────────────┐
│  STRUCT (AND)                                               │
│                                                             │
│  struct Point { x: f64, y: f64 }                            │
│                                                             │
│  A Point has BOTH an x AND a y.                             │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ENUM (OR)                                                  │
│                                                             │
│  enum Shape {                                               │
│    Circle(f64),                                             │
│    Rectangle(f64, f64),                                     │
│  }                                                          │
│                                                             │
│  A Shape is EITHER a Circle OR a Rectangle.                 │
│  Never both.                                                │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  CLASS HIERARCHY (OO)                                       │
│                                                             │
│  abstract class Shape { }                                   │
│  class Circle extends Shape { }                             │
│  class Rectangle extends Shape { }                          │
│                                                             │
│  Equivalent to enum in Rust, but:                           │
│  - No exhaustive matching                                   │
│  - Adding a subclass doesn't break compilation              │
│  - Data and behavior mixed in the class                     │
│                                                             │
│  Rust enums: data variants + separate impl blocks.          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Enum definitionenum Name { Variant1, Variant2 }
Unit variantNo data
Tuple variantVariant(T1, T2)
Struct variantVariant { field: T }
Recursive variantVariant(Box<Self>)
Matchmatch value { ... }
Single variantif let Variant(x) = value
Extract or divergelet Variant(x) = value else { }
Option<T>Some(T) or None
Result<T, E>Ok(T) or Err(E)
ExhaustivenessCompiler enforces all variants
MethodsDefined in impl blocks

Key takeaways:

  • An enum value is exactly one of its variants. Unlike a struct, which has all its fields, an enum has one variant at a time. The compiler tracks which variant a value is.
  • Each variant can carry different data. Unit variants carry none. Tuple variants carry unnamed values by position. Struct variants carry named fields. The compiler knows the type and arity of each.
  • Pattern matching extracts variant data safely. You cannot access a variant’s data without matching on it. The match expression is exhaustive, and if let and let else handle single-variant cases.
  • Option<T> replaces null. A value that might be absent is an Option, which is either Some(T) or None. The compiler forces you to handle both cases.
  • Result<T, E> replaces exceptions. A fallible operation returns a Result, which is either Ok(T) or Err(E). The ? operator propagates errors without explicit match blocks.
  • Enums replace class hierarchies. A base class with subclasses becomes an enum with variants. Virtual method dispatch becomes match. This makes the set of possibilities explicit and exhaustively checkable.
  • Recursive enums need Box. An enum that contains itself as a variant must wrap the recursive field in Box<Self> to give the type a finite size.
  • Adding a variant breaks compilation. When you add a new variant, every match that does not handle it becomes an error. This is a feature: the compiler tells you everywhere the new case needs consideration.

Remember: Rust enums are not named constants. They are a full algebraic data type—a type that is exactly one of several alternatives, each with its own data. Option and Result are the most important examples, but the pattern applies everywhere: state machines, AST nodes, JSON values, network messages, user input. The match expression is the primary tool for working with enums, and the compiler’s exhaustiveness check ensures that every variant is handled. Where an object-oriented language would use a class hierarchy, Rust uses an enum and an impl block. The result is data that is explicit, patterns that are checked, and behavior that is separate from representation.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!