Enums in Rust
Enums in Rust are far more powerful than enums in C or Java. In Rust, each variant of an enum can carry its own data — different types, different shapes. This makes enums the right tool for modeling values that can be one of several distinct things, each potentially holding different information.
Basic Enums
A basic enum lists a set of named variants. You define it with the enum keyword:
enum Direction {
North,
South,
East,
West,
}
let heading = Direction::North;
match heading {
Direction::North => println!("Going north"),
Direction::South => println!("Going south"),
Direction::East => println!("Going east"),
Direction::West => println!("Going west"),
}Enums with Data
Each variant can hold data — and different variants can hold completely different types of data. This is what makes Rust enums special:
enum Message {
Quit, // no data
Move { x: i32, y: i32 }, // named fields (like a struct)
Write(String), // one String value
ChangeColor(i32, i32, i32), // three i32 values
}
let m1 = Message::Quit;
let m2 = Message::Move { x: 10, y: 20 };
let m3 = Message::Write(String::from("hello"));
let m4 = Message::ChangeColor(255, 128, 0);Think of each variant as its own mini-type. Message::Move is like an anonymous struct. Message::Write is like a tuple struct. Message::Quit is like a unit struct. All of them share the single type Message.
Enums Can Hold Any Type
Enum variants can hold strings, integers, tuples, structs, Vecs, Boxes — any Rust type at all:
#[derive(Debug)]
struct Point { x: f64, y: f64 }
#[derive(Debug)]
enum Shape {
Circle { center: Point, radius: f64 },
Rectangle { top_left: Point, bottom_right: Point },
Triangle(Point, Point, Point),
Label(String),
}
let s1 = Shape::Circle {
center: Point { x: 0.0, y: 0.0 },
radius: 5.0,
};
let s2 = Shape::Label(String::from("origin"));
println!("{:?}", s1);
println!("{:?}", s2);impl Blocks on Enums
Just like structs, enums can have methods defined in an impl block:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
match self {
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: rgb({}, {}, {})", r, g, b)
}
}
}
fn is_quit(&self) -> bool {
matches!(self, Message::Quit)
}
}
let msg = Message::Write(String::from("hello world"));
msg.call();
println!("Is quit: {}", msg.is_quit());Pattern Matching with match
The match expression is the primary way to work with enums. It must be exhaustive — every possible variant must be handled:
#[derive(Debug)]
enum Coin {
Penny,
Nickel,
Dime,
Quarter(String), // Quarter holds a state name
}
fn value_in_cents(coin: &Coin) -> u32 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("Quarter from {}!", state);
25
}
}
}
let q = Coin::Quarter(String::from("Alaska"));
println!("{} cents", value_in_cents(&q));The Option Enum
Rust has no null. Instead, the standard library provides Option<T>, which forces you to explicitly handle the absence of a value:
// Defined in the standard library (no need to import)
// enum Option<T> {
// None,
// Some(T),
// }
fn find_user(id: u32) -> Option<String> {
if id == 1 {
Some(String::from("Alice"))
} else {
None
}
}
let result = find_user(1);
match result {
Some(name) => println!("Found: {}", name),
None => println!("User not found"),
}Common Option methods that avoid verbose match blocks:
let some_val: Option<i32> = Some(42);
let none_val: Option<i32> = None;
// unwrap_or: provide a default
println!("{}", some_val.unwrap_or(0)); // 42
println!("{}", none_val.unwrap_or(0)); // 0
// map: transform the inner value if Some
let doubled = some_val.map(|v| v * 2);
println!("{:?}", doubled); // Some(84)
// is_some / is_none
println!("has value: {}", some_val.is_some()); // true
println!("is empty: {}", none_val.is_none()); // trueThe Result Enum
Result<T, E> is Rust's error-handling type. Instead of throwing exceptions, functions that can fail return Result. The caller is forced to deal with errors:
// Defined in the standard library
// enum Result<T, E> {
// Ok(T),
// Err(E),
// }
use std::num::ParseIntError;
fn parse_age(s: &str) -> Result<u32, ParseIntError> {
let n: u32 = s.trim().parse()?;
Ok(n)
}
match parse_age("25") {
Ok(age) => println!("Age is {}", age),
Err(e) => println!("Parse error: {}", e),
}
match parse_age("abc") {
Ok(age) => println!("Age is {}", age),
Err(e) => println!("Parse error: {}", e),
}Common Result methods:
let ok: Result<i32, &str> = Ok(10);
let err: Result<i32, &str> = Err("oops");
println!("{}", ok.unwrap_or(0)); // 10
println!("{}", err.unwrap_or(0)); // 0
println!("{:?}", ok.map(|v| v * 2)); // Ok(20)
println!("{}", ok.is_ok()); // true
println!("{}", err.is_err()); // trueDeriving Traits on Enums
Like structs, enums can derive common traits:
#[derive(Debug, Clone, PartialEq)]
enum Status {
Active,
Inactive,
Pending(String),
}
let s1 = Status::Active;
let s2 = Status::Active;
let s3 = Status::Pending(String::from("review"));
println!("{:?}", s1);
println!("s1 == s2: {}", s1 == s2);
println!("s1 == s3: {}", s1 == s3);
let s4 = s3.clone();
println!("Cloned: {:?}", s4);Enums as State Machines
Enums are a natural fit for modeling state machines. Each state is a variant, and transitions are methods that consume the old state and produce the new one:
#[derive(Debug)]
enum TrafficLight {
Red,
Yellow,
Green,
}
impl TrafficLight {
fn next(self) -> Self {
match self {
TrafficLight::Red => TrafficLight::Green,
TrafficLight::Green => TrafficLight::Yellow,
TrafficLight::Yellow => TrafficLight::Red,
}
}
fn duration_seconds(&self) -> u32 {
match self {
TrafficLight::Red => 60,
TrafficLight::Green => 45,
TrafficLight::Yellow => 5,
}
}
}
let mut light = TrafficLight::Red;
for _ in 0..4 {
println!("{:?} — {} seconds", light, light.duration_seconds());
light = light.next();
}Enums vs Constants
Feature | Enum | Constant |
|---|---|---|
Type safety | Enforced — variants are distinct types | No — just a value of a primitive type |
Data attachment | Each variant can carry data | Constants hold a single fixed value |
Exhaustiveness | match forces you to handle all variants | No such guarantee |
Pattern matching | Full match / if let / while let support | Only equality checks |
Documentation | Variants are self-documenting | Need comments for intent |
Real-World Example: Payment Method
#[derive(Debug, Clone)]
enum PaymentMethod {
Cash,
CreditCard {
number: String,
expiry: String,
},
PayPal(String), // holds email address
Crypto { coin: String, wallet: String },
}
impl PaymentMethod {
fn display_name(&self) -> String {
match self {
PaymentMethod::Cash => String::from("Cash"),
PaymentMethod::CreditCard { number, .. } => {
let last4 = &number[number.len().saturating_sub(4)..];
format!("Card ending in {}", last4)
}
PaymentMethod::PayPal(email) => {
format!("PayPal ({})", email)
}
PaymentMethod::Crypto { coin, .. } => {
format!("{} wallet", coin)
}
}
}
fn requires_online(&self) -> bool {
match self {
PaymentMethod::Cash => false,
_ => true,
}
}
}
fn process_payment(amount: f64, method: &PaymentMethod) {
println!(
"Processing ${:.2} via {} (online: {})",
amount,
method.display_name(),
method.requires_online()
);
}
let methods = vec![
PaymentMethod::Cash,
PaymentMethod::CreditCard {
number: String::from("4111111111111234"),
expiry: String::from("12/27"),
},
PaymentMethod::PayPal(String::from("user@example.com")),
];
for method in &methods {
process_payment(99.99, method);
}Real-World Example: HTTP Method
#[derive(Debug, Clone, PartialEq)]
enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
Head,
Options,
}
impl HttpMethod {
fn is_safe(&self) -> bool {
matches!(self, HttpMethod::Get | HttpMethod::Head | HttpMethod::Options)
}
fn is_idempotent(&self) -> bool {
matches!(
self,
HttpMethod::Get
| HttpMethod::Put
| HttpMethod::Delete
| HttpMethod::Head
| HttpMethod::Options
)
}
}
let methods = [
HttpMethod::Get,
HttpMethod::Post,
HttpMethod::Put,
HttpMethod::Delete,
];
for m in &methods {
println!(
"{:?} — safe: {}, idempotent: {}",
m,
m.is_safe(),
m.is_idempotent()
);
}if let — Concise Single-Variant Matching
When you only care about one variant, if let is more concise than a full match:
let config: Option<String> = Some(String::from("debug"));
// Verbose with match
match &config {
Some(val) => println!("Config: {}", val),
None => {},
}
// Concise with if let
if let Some(val) = &config {
println!("Config: {}", val);
}
// Also works with Result
let result: Result<i32, &str> = Ok(42);
if let Ok(n) = result {
println!("Got: {}", n);
}Enums as Algebraic Data Types
Rust enums are what functional programmers call sum types (also known as tagged unions or algebraic data types). A value of an enum type is exactly one of its variants at any time — the compiler tracks which one and refuses to compile code that ignores a variant.
Structs are product types — they hold ALL their fields simultaneously
Enums are sum types — they hold exactly ONE variant at a time
Together they let you model any domain accurately
The compiler's exhaustiveness check means you can't forget a case
This is why Rust has no NullPointerException class of bugs