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
| Form | Syntax | Example |
|---|---|---|
| Unit | Name | Direction::North |
| Tuple | Name(T1, T2) | Shape::Rectangle(f64, f64) |
| Struct | Name { field: T } | Message::Move { x: i32, y: i32 } |
| Recursive | Name(Box<Self>) | Expr::Add(Box<Expr>, Box<Expr>) |
Creating and Matching
| Operation | Syntax |
|---|---|
| Create unit | Direction::North |
| Create tuple | Shape::Circle(3.0) |
| Create struct | Message::Move { x: 1, y: 2 } |
| Match | match value { ... } |
| Match one | if let Some(x) = value { } |
| Extract or diverge | let Some(x) = value else { } |
Standard Enums
| Enum | Variants | Purpose |
|---|---|---|
Option<T> | Some(T), None | Presence or absence |
Result<T, E> | Ok(T), Err(E) | Success or failure |
Ordering | Less, Equal, Greater | Comparison result |
Cow<'a, B> | Borrowed, Owned | Clone-on-write |
Match Patterns
| Pattern | Matches |
|---|---|
Variant | Unit variant |
Variant(x) | Tuple variant |
Variant { field } | Struct variant |
Variant(_) | Variant ignoring data |
_ | Any value |
Some(x) if x > 0 | Guard condition |
x @ 1..=5 | Binding 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Non-exhaustive match | Missing variant | Add the missing arm |
| Infinite size | Recursive enum without Box | Wrap recursive field in Box |
| Cannot access variant data | Did not match first | Use match, if let, or let else |
| Move error in match | Matching by value consumes | Match on &value or use ref |
unwrap panic | None or Err encountered | Use ? or handle explicitly |
| Unreachable pattern | Earlier arm catches all | Reorder arms |
| Type mismatch in variant | Wrong data type | Check 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
| Item | Value |
|---|---|
| Enum definition | enum Name { Variant1, Variant2 } |
| Unit variant | No data |
| Tuple variant | Variant(T1, T2) |
| Struct variant | Variant { field: T } |
| Recursive variant | Variant(Box<Self>) |
| Match | match value { ... } |
| Single variant | if let Variant(x) = value |
| Extract or diverge | let Variant(x) = value else { } |
Option<T> | Some(T) or None |
Result<T, E> | Ok(T) or Err(E) |
| Exhaustiveness | Compiler enforces all variants |
| Methods | Defined 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
matchexpression is exhaustive, andif letandlet elsehandle single-variant cases. Option<T>replaces null. A value that might be absent is anOption, which is eitherSome(T)orNone. The compiler forces you to handle both cases.Result<T, E>replaces exceptions. A fallible operation returns aResult, which is eitherOk(T)orErr(E). The?operator propagates errors without explicitmatchblocks.- 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 inBox<Self>to give the type a finite size. - Adding a variant breaks compilation. When you add a new variant, every
matchthat 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!