Enums in Rust are far more powerful than in most other languages. Each variant can carry different types and amounts of data, making enums Rust’s primary tool for modeling “sum types” — values that can be one of several distinct shapes. Combined with pattern matching, they enable exhaustive, compile-checked control flow that eliminates entire classes of runtime errors.
Defining Enums
An enum defines a type with a fixed set of variants:
enum Direction {
North,
South,
East,
West,
}
fn describe(d: Direction) -> &'static str {
match d {
Direction::North => "heading north",
Direction::South => "heading south",
Direction::East => "heading east",
Direction::West => "heading west",
}
}
Variants with Data
Each variant can hold different data — tuples, structs, or nothing:
#[derive(Debug)]
enum Shape {
Circle { radius: f64 }, // Named fields
Rectangle { width: f64, height: f64 }, // Named fields
Triangle(f64, f64, f64), // Unnamed (tuple-style)
Point, // No data
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle(a, b, c) => {
// Heron's formula
let s = (a + b + c) / 2.0;
(s * (s - a) * (s - b) * (s - c)).sqrt()
}
Shape::Point => 0.0,
}
}
fn perimeter(&self) -> f64 {
match self {
Shape::Circle { radius } => 2.0 * std::f64::consts::PI * radius,
Shape::Rectangle { width, height } => 2.0 * (width + height),
Shape::Triangle(a, b, c) => a + b + c,
Shape::Point => 0.0,
}
}
}
fn main() {
let shapes = vec![
Shape::Circle { radius: 3.0 },
Shape::Rectangle { width: 4.0, height: 6.0 },
Shape::Triangle(3.0, 4.0, 5.0),
];
for shape in &shapes {
println!("{:?}: area={:.2}, perimeter={:.2}", shape, shape.area(), shape.perimeter());
}
}
Enum Methods with impl
Enums are types just like structs — they can have methods:
#[derive(Debug, PartialEq)]
enum TrafficLight {
Red,
Yellow,
Green,
}
impl TrafficLight {
fn duration_secs(&self) -> u32 {
match self {
TrafficLight::Red => 60,
TrafficLight::Yellow => 5,
TrafficLight::Green => 45,
}
}
fn next(&self) -> TrafficLight {
match self {
TrafficLight::Red => TrafficLight::Green,
TrafficLight::Green => TrafficLight::Yellow,
TrafficLight::Yellow => TrafficLight::Red,
}
}
fn is_stop(&self) -> bool {
matches!(self, TrafficLight::Red | TrafficLight::Yellow)
}
}
fn main() {
let mut light = TrafficLight::Red;
for _ in 0..5 {
println!("{:?} ({}s, stop={})", light, light.duration_secs(), light.is_stop());
light = light.next();
}
}
Option<T> — The Null Safety Enum
Option is Rust’s replacement for null. It’s defined in the standard library as:
enum Option<T> {
Some(T),
None,
}
It forces you to handle the “missing value” case at compile time:
fn find_user(id: u32) -> Option<String> {
match id {
1 => Some("Alice".to_string()),
2 => Some("Bob".to_string()),
_ => None,
}
}
fn main() {
// Explicit match
match find_user(1) {
Some(name) => println!("Found: {}", name),
None => println!("Not found"),
}
// Concise with if let
if let Some(name) = find_user(2) {
println!("Found: {}", name);
}
// Use default value
let name = find_user(99).unwrap_or_else(|| "Unknown".to_string());
println!("{}", name); // Unknown
// Transform without unwrapping
let length = find_user(1).map(|n| n.len());
println!("{:?}", length); // Some(5)
// Chain operations
let upper = find_user(1)
.filter(|n| n.len() > 3)
.map(|n| n.to_uppercase());
println!("{:?}", upper); // Some("ALICE")
}
Essential Option Methods
fn main() {
let some: Option<i32> = Some(42);
let none: Option<i32> = None;
// unwrap_or / unwrap_or_else / unwrap_or_default
println!("{}", some.unwrap_or(0)); // 42
println!("{}", none.unwrap_or(0)); // 0
println!("{}", none.unwrap_or_default()); // 0 (i32 default)
// map — transform the inner value
println!("{:?}", some.map(|x| x * 2)); // Some(84)
// and_then — chain operations that might fail (flatMap)
let result = some
.and_then(|x| if x > 0 { Some(x.to_string()) } else { None })
.and_then(|s| s.parse::<f64>().ok());
println!("{:?}", result); // Some(42.0)
// ok_or — convert to Result
let r: Result<i32, &str> = some.ok_or("missing value");
println!("{:?}", r); // Ok(42)
// is_some / is_none
println!("{} {}", some.is_some(), none.is_none()); // true true
}
Result<T, E> — Error Handling Enum
Result models operations that can succeed or fail:
use std::num::ParseIntError;
use std::fs;
fn parse_port(s: &str) -> Result<u16, ParseIntError> {
s.parse::<u16>()
}
fn read_config(path: &str) -> Result<String, std::io::Error> {
fs::read_to_string(path)
}
fn main() {
// Pattern match on Result
match parse_port("8080") {
Ok(port) => println!("Port: {}", port),
Err(e) => println!("Invalid port: {}", e),
}
// map_err — transform the error type
let r = parse_port("abc")
.map_err(|e| format!("Parse failed: {}", e));
println!("{:?}", r); // Err("Parse failed: ...")
// unwrap_or_else — provide fallback on error
let port = parse_port("xyz").unwrap_or_else(|_| 3000);
println!("Using port: {}", port); // 3000
}
The ? operator is syntactic sugar for early-returning on Err:
use std::io;
use std::num::ParseIntError;
use std::fmt;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {}", e),
AppError::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
impl From<io::Error> for AppError { fn from(e: io::Error) -> Self { AppError::Io(e) } }
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::Parse(e) } }
fn read_port_from_file(path: &str) -> Result<u16, AppError> {
let content = std::fs::read_to_string(path)?; // io::Error auto-converted
let port = content.trim().parse::<u16>()?; // ParseIntError auto-converted
Ok(port)
}
The match Expression
match is exhaustive — the compiler ensures all variants are handled:
#[derive(Debug)]
enum Command {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
fn execute(cmd: Command) {
match cmd {
Command::Quit => {
println!("Quitting");
}
Command::Move { x, y } => {
println!("Moving to ({}, {})", x, y);
}
Command::Write(text) => {
println!("Writing: {}", text);
}
Command::ChangeColor(r, g, b) => {
println!("Color: rgb({}, {}, {})", r, g, b);
}
}
}
Match Guards
Add conditions to arms with if:
fn categorize(n: i32) -> &'static str {
match n {
x if x < 0 => "negative",
0 => "zero",
x if x % 2 == 0 => "positive even",
_ => "positive odd",
}
}
fn main() {
for &n in &[-3, 0, 4, 7] {
println!("{}: {}", n, categorize(n));
}
}
Multiple Patterns with |
fn is_vowel(c: char) -> bool {
matches!(c, 'a' | 'e' | 'i' | 'o' | 'u' | 'A' | 'E' | 'I' | 'O' | 'U')
}
fn classify_char(c: char) -> &'static str {
match c {
'a'..='z' | 'A'..='Z' => "letter",
'0'..='9' => "digit",
' ' | '\t' | '\n' => "whitespace",
_ => "other",
}
}
Range Patterns
fn letter_grade(score: u32) -> &'static str {
match score {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
_ => "F",
}
}
Binding with @
Capture a value while also matching a pattern:
fn check_value(n: u32) {
match n {
// Bind n to `val` if it's in range 1..=10
val @ 1..=10 => println!("{} is between 1 and 10", val),
val @ 11..=20 => println!("{} is between 11 and 20", val),
other => println!("{} is out of range", other),
}
}
Destructuring Nested Structures
#[derive(Debug)]
struct Point { x: i32, y: i32 }
#[derive(Debug)]
enum Message {
Move(Point),
Color { r: u8, g: u8, b: u8 },
}
fn process(msg: Message) {
match msg {
Message::Move(Point { x, y }) => {
println!("Move to x={}, y={}", x, y);
}
Message::Color { r, g: 0, b: 0 } => {
println!("Purely red: {}", r);
}
Message::Color { r, g, b } => {
println!("Color: ({}, {}, {})", r, g, b);
}
}
}
The .. Ignore Pattern
#[derive(Debug)]
struct Config {
host: String,
port: u16,
debug: bool,
timeout: u32,
}
fn main() {
let cfg = Config {
host: "localhost".to_string(),
port: 8080,
debug: true,
timeout: 30,
};
// Only care about host and port
let Config { host, port, .. } = cfg;
println!("Connecting to {}:{}", host, port);
}
if let and while let
For single-variant matching without the boilerplate of match:
fn main() {
let value: Option<i32> = Some(42);
// if let — matches one variant
if let Some(n) = value {
println!("Got {}", n);
}
// if let with else
if let Some(n) = value {
println!("Some: {}", n);
} else {
println!("None");
}
// Chained if let else if let
let result: Result<i32, &str> = Ok(10);
if let Ok(n) = result {
println!("OK: {}", n);
} else if let Err(e) = result {
println!("Err: {}", e);
}
// while let — drain a stack
let mut stack = vec![1, 2, 3, 4, 5];
while let Some(top) = stack.pop() {
print!("{} ", top); // 5 4 3 2 1
}
println!();
}
let else — Early Return on Non-Match
Rust 1.65+ introduced let else, which binds a pattern or diverges (returns/panics):
fn process_user_id(input: &str) -> Result<(), String> {
let Ok(id) = input.parse::<u64>() else {
return Err(format!("Invalid ID: {}", input));
};
println!("Processing user {}", id);
Ok(())
}
fn main() {
process_user_id("42").unwrap();
process_user_id("abc").unwrap_err();
}
This replaces the common match-with-early-return pattern more concisely.
matches! Macro
A concise way to test if a value matches a pattern, returning bool:
#[derive(Debug)]
enum Status { Active, Inactive, Pending }
fn main() {
let s = Status::Active;
// Instead of: match s { Status::Active => true, _ => false }
println!("{}", matches!(s, Status::Active)); // true
println!("{}", matches!(s, Status::Active | Status::Pending)); // true
// With guards
let n = Some(42i32);
println!("{}", matches!(n, Some(x) if x > 0)); // true
}
State Machine Pattern
Enums are ideal for representing state machines, where transitions are type-safe:
#[derive(Debug, Clone, PartialEq)]
enum OrderState {
Pending { items: Vec<String> },
Processing { order_id: u64, items: Vec<String> },
Shipped { tracking_number: String },
Delivered { delivered_at: String },
Cancelled { reason: String },
}
impl OrderState {
fn confirm(self, order_id: u64) -> Result<OrderState, String> {
match self {
OrderState::Pending { items } => {
Ok(OrderState::Processing { order_id, items })
}
other => Err(format!("Cannot confirm order in state {:?}", other)),
}
}
fn ship(self, tracking_number: String) -> Result<OrderState, String> {
match self {
OrderState::Processing { .. } => {
Ok(OrderState::Shipped { tracking_number })
}
other => Err(format!("Cannot ship order in state {:?}", other)),
}
}
fn is_terminal(&self) -> bool {
matches!(self, OrderState::Delivered { .. } | OrderState::Cancelled { .. })
}
}
fn main() {
let order = OrderState::Pending {
items: vec!["Widget".to_string(), "Gadget".to_string()],
};
let order = order.confirm(1001).unwrap();
println!("{:?}", order);
let order = order.ship("TRACK-XYZ-123".to_string()).unwrap();
println!("{:?}", order);
println!("Terminal: {}", order.is_terminal()); // false
}
Summary
| Feature | Purpose |
|---|---|
| Enum variants with data | Model sum types — values that are one of N shapes |
match |
Exhaustive pattern matching — compiler ensures all cases handled |
Match guards (if) |
Add conditions to match arms |
@ bindings |
Capture value while testing pattern |
Option<T> |
Null-safe optional values |
Result<T, E> |
Explicit error handling without exceptions |
if let / while let |
Concise single-pattern matching |
let else |
Bind or diverge — clean early returns |
matches! |
Boolean pattern test |
Rust’s enums and pattern matching together eliminate null pointer exceptions, unhandled error cases, and invalid state transitions — not through runtime checks, but through the type system itself.
Comments