Rust enums
last modified October 25, 2026
In this article we show how to work with enums in Rust.
An enum (enumeration) is a type that has a fixed set of possible values, called variants. A value of an enum type is always exactly one of its variants.
Rust enums are more powerful than enums in C or Java. Each variant can carry
its own data, of a different type and shape than the other variants. Such
types are known as sum types or tagged unions. Enums work together
with the match expression, which forces us to handle every
variant. Two of the most important types in the standard library,
Option and Result, are enums.
The examples were tested with Rust 1.97 and the 2024 edition. They are run as
Cargo projects with cargo run -q.
A simple enum
An enum is defined with the enum keyword, followed by its name
and a list of variants.
enum Direction {
North,
East,
South,
West,
}
fn describe(d: Direction) -> &'static str {
match d {
Direction::North => "going up",
Direction::East => "going right",
Direction::South => "going down",
Direction::West => "going left",
}
}
fn main() {
let d = Direction::West;
println!("{}", describe(d));
for d in [Direction::North, Direction::East, Direction::South] {
println!("{}", describe(d));
}
}
We define a Direction enum with four variants and a function
that describes each of them.
enum Direction {
North,
East,
South,
West,
}
The variants are listed in curly braces and separated by commas. Like type names, variant names use CamelCase.
match d {
Direction::North => "going up",
Direction::East => "going right",
Direction::South => "going down",
Direction::West => "going left",
}
Variants are namespaced by the enum name: Direction::North. The
match expression must cover all variants. If we added a new
variant to the enum, the program would not compile until the
match handles it as well.
$ cargo run -q going left going up going right going down
Importing variants
The variants can be brought into scope with a use declaration,
so that they can be written without the enum prefix.
#[derive(Debug)]
enum Size {
Small,
Medium,
Large,
}
use Size::*;
fn price(s: &Size) -> u32 {
match s {
Small => 3,
Medium => 4,
Large => 5,
}
}
fn main() {
let order = [Small, Large, Large, Medium];
for s in &order {
println!("{:?}: {}", s, price(s));
}
let total: u32 = order.iter().map(price).sum();
println!("total: {total}");
}
The program computes the price of an order of drinks.
use Size::*;
The glob import makes Small, Medium, and
Large available directly. This is convenient for enums that are
used heavily in one module, but it should be used with care: a misspelled
variant name in a match arm would become a catch-all variable
binding instead of an error. The compiler warns about such cases.
let total: u32 = order.iter().map(price).sum();
The price function takes a &Size, which is what
iter produces, so it can be passed to map by name.
$ cargo run -q Small: 3 Large: 5 Large: 5 Medium: 4 total: 17
Discriminants
Enums whose variants carry no data can be given explicit integer values,
called discriminants, and cast to integers with as.
#[derive(Debug, Clone, Copy)]
enum HttpStatus {
Ok = 200,
NotFound = 404,
InternalError = 500,
}
#[derive(Debug, Clone, Copy)]
enum Weekday {
Monday = 1,
Tuesday,
Wednesday,
}
fn main() {
let st = HttpStatus::NotFound;
println!("{:?} has code {}", st, st as i32);
println!("{:?} has code {}", HttpStatus::Ok, HttpStatus::Ok as i32);
println!("{:?} has code {}", HttpStatus::InternalError, HttpStatus::InternalError as u16);
for d in [Weekday::Monday, Weekday::Tuesday, Weekday::Wednesday] {
println!("{:?} = {}", d, d as u8);
}
}
We define two C-like enums.
enum HttpStatus {
Ok = 200,
NotFound = 404,
InternalError = 500,
}
Each variant gets an explicit value.
enum Weekday {
Monday = 1,
Tuesday,
Wednesday,
}
If a variant has no explicit value, it gets the value of the previous variant plus one. Without any explicit values, the numbering starts at zero.
println!("{:?} has code {}", st, st as i32);
The as cast converts a variant to its discriminant. The
conversion in the other direction, from an integer to an enum, is not
provided automatically, because not every integer is a valid variant; we show
how to implement it later with TryFrom.
$ cargo run -q NotFound has code 404 Ok has code 200 InternalError has code 500 Monday = 1 Tuesday = 2 Wednesday = 3
Variants with data
Each variant can hold data. A variant can have no data, unnamed fields like a tuple struct, or named fields like a regular struct.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
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 set to #{r:02x}{g:02x}{b:02x}"),
}
}
fn main() {
let messages = vec![
Message::Write(String::from("hello")),
Message::Move { x: 10, y: -5 },
Message::ChangeColor(255, 128, 0),
Message::Quit,
];
for msg in &messages {
process(msg);
}
}
The Message enum has four variants of different shapes.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
Quit has no data, Move has two named fields,
Write holds a string, and ChangeColor holds three
numbers. Defining four separate structs would give four unrelated types; the
enum lets us store all kinds of messages in one vector and pass them to one
function.
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 set to #{r:02x}{g:02x}{b:02x}"),
}
The patterns in the match arms extract the data of each variant.
The data is accessible only after we check which variant we have; there is no
way to read the fields of the wrong variant.
$ cargo run -q writing 'hello' moving to 10, -5 color set to #ff8000 quitting
Methods on enums
Like structs, enums can have methods defined in an impl block.
use std::f64::consts::PI;
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle { a: f64, b: f64, c: f64 },
}
impl Shape {
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => 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()
}
}
}
fn name(&self) -> &str {
match self {
Shape::Circle(_) => "circle",
Shape::Rectangle(..) => "rectangle",
Shape::Triangle { .. } => "triangle",
}
}
}
fn main() {
let shapes = [
Shape::Circle(1.5),
Shape::Rectangle(3.0, 4.0),
Shape::Triangle { a: 3.0, b: 4.0, c: 5.0 },
];
for s in &shapes {
println!("{:<10} {:.2}", s.name(), s.area());
}
}
The Shape enum gets two methods.
fn area(&self) -> f64 {
match self {
Shape::Circle(r) => 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()
}
}
}
A method usually matches on self and handles each variant. The
area of the triangle is computed with Heron's formula.
Shape::Circle(_) => "circle",
Shape::Rectangle(..) => "rectangle",
Shape::Triangle { .. } => "triangle",
When the data is not needed, it is ignored with _ for a single
field or .. for all fields.
$ cargo run -q circle 7.07 rectangle 12.00 triangle 6.00
Deriving traits
Enums can derive the same standard traits as structs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Priority {
Low,
Medium,
High,
Critical,
}
fn main() {
let p = Priority::High;
println!("{:?}", p);
println!("{}", p == Priority::High);
println!("{}", p > Priority::Medium);
println!("{}", Priority::Low < Priority::Critical);
let mut tasks = vec![Priority::Medium, Priority::Critical, Priority::Low, Priority::High];
tasks.sort();
println!("{:?}", tasks);
let max = tasks.iter().max().unwrap();
println!("max: {:?}", max);
}
The Priority enum derives comparison and hashing traits.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
Since the enum carries no heap data, it can be Copy; it is
passed around by value like an integer. Hash together with
Eq allows using it as a key in a HashMap.
println!("{}", p > Priority::Medium);
The derived PartialOrd and Ord order the variants
by their declaration order: Low is the smallest and
Critical the largest. This also makes the vector sortable and
lets max find the highest priority.
$ cargo run -q High true true true [Low, Medium, High, Critical] max: Critical
The matches! macro
The matches! macro checks whether a value matches a pattern and
returns a bool.
#[derive(Debug)]
enum Token {
Number(i64),
Plus,
Minus,
Star,
LParen,
RParen,
}
fn main() {
let tokens = [
Token::LParen,
Token::Number(3),
Token::Plus,
Token::Number(42),
Token::RParen,
Token::Star,
Token::Number(-7),
Token::Minus,
];
let operators = tokens
.iter()
.filter(|t| matches!(t, Token::Plus | Token::Minus | Token::Star))
.count();
println!("operators: {operators}");
let big = tokens.iter().any(|t| matches!(t, Token::Number(n) if *n > 40));
println!("number above 40: {big}");
let parens: Vec<_> = tokens
.iter()
.filter(|t| matches!(t, Token::LParen | Token::RParen))
.collect();
println!("{:?}", parens);
}
We analyze a list of tokens produced by a simple lexer.
.filter(|t| matches!(t, Token::Plus | Token::Minus | Token::Star))
The | operator in a pattern matches any of several variants.
Without the macro we would have to write a full match with
true and false arms. Note that ==
does not work here, because Token does not implement
PartialEq.
let big = tokens.iter().any(|t| matches!(t, Token::Number(n) if *n > 40));
The pattern can include a guard. Here we check whether any number token is greater than 40.
$ cargo run -q operators: 3 number above 40: true [LParen, RParen]
if let and let else
When we are interested in a single variant, a full match is
verbose. The if let and let else constructs handle
one pattern.
enum Command {
Add(i32),
Sub(i32),
Print,
Reset,
}
fn parse(s: &str) -> Option<Command> {
let mut parts = s.split_whitespace();
let cmd = parts.next()?;
match cmd {
"add" => Some(Command::Add(parts.next()?.parse().ok()?)),
"sub" => Some(Command::Sub(parts.next()?.parse().ok()?)),
"print" => Some(Command::Print),
"reset" => Some(Command::Reset),
_ => None,
}
}
fn main() {
let input = ["add 5", "add 10", "sub 3", "print", "mul 2", "reset", "add x", "print"];
let mut acc = 0;
for line in input {
let Some(cmd) = parse(line) else {
println!("invalid command: {line}");
continue;
};
if let Command::Print = cmd {
println!("acc = {acc}");
continue;
}
match cmd {
Command::Add(n) => acc += n,
Command::Sub(n) => acc -= n,
Command::Reset => acc = 0,
Command::Print => unreachable!(),
}
}
}
The program parses and executes simple commands that change an accumulator.
"add" => Some(Command::Add(parts.next()?.parse().ok()?)),
The parser returns Option<Command>. The ?
operator returns None early when the argument is missing or is
not a number.
let Some(cmd) = parse(line) else {
println!("invalid command: {line}");
continue;
};
The let else statement binds cmd if the pattern
matches. Otherwise the else block runs; it must leave the
current scope, here with continue.
if let Command::Print = cmd {
println!("acc = {acc}");
continue;
}
The if let expression runs its block only for the
Print variant.
Command::Print => unreachable!(),
The match must still be exhaustive. Since the Print
case was already handled, we mark it with unreachable!, which
panics if the arm is ever reached.
$ cargo run -q acc = 12 invalid command: mul 2 invalid command: add x acc = 0
Recursive enums
A variant can contain a value of the enum itself, which makes it possible to
model tree structures such as expressions. The recursive value must be put
behind a pointer, usually a Box.
enum Expr {
Num(f64),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Neg(Box<Expr>),
}
use Expr::*;
fn eval(e: &Expr) -> f64 {
match e {
Num(n) => *n,
Add(a, b) => eval(a) + eval(b),
Mul(a, b) => eval(a) * eval(b),
Neg(a) => -eval(a),
}
}
fn show(e: &Expr) -> String {
match e {
Num(n) => n.to_string(),
Add(a, b) => format!("({} + {})", show(a), show(b)),
Mul(a, b) => format!("{} * {}", show(a), show(b)),
Neg(a) => format!("-{}", show(a)),
}
}
fn main() {
let e = Mul(
Box::new(Add(Box::new(Num(2.0)), Box::new(Num(3.0)))),
Box::new(Neg(Box::new(Num(4.0)))),
);
println!("{} = {}", show(&e), eval(&e));
}
We build and evaluate the expression (2 + 3) * -4.
enum Expr {
Num(f64),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Neg(Box<Expr>),
}
Without the Box, the compiler could not determine the size of
Expr, because it would contain itself infinitely. A
Box is a pointer of a fixed size that stores its value on the
heap.
fn eval(e: &Expr) -> f64 {
match e {
Num(n) => *n,
Add(a, b) => eval(a) + eval(b),
Mul(a, b) => eval(a) * eval(b),
Neg(a) => -eval(a),
}
}
Recursive data is naturally processed by recursive functions. A
&Box<Expr> is automatically dereferenced to
&Expr when passed to eval.
$ cargo run -q (2 + 3) * -4 = -20
A state machine
Enums are well suited for modelling states. Each variant stores the data that is relevant in that state only.
enum Order {
Created { id: u32 },
Paid { id: u32, amount: f64 },
Shipped { id: u32, tracking: String },
Cancelled { id: u32 },
}
impl Order {
fn pay(self, amount: f64) -> Order {
match self {
Order::Created { id } => Order::Paid { id, amount },
other => other,
}
}
fn ship(self, tracking: &str) -> Order {
match self {
Order::Paid { id, .. } => Order::Shipped { id, tracking: tracking.to_string() },
other => other,
}
}
fn status(&self) -> String {
match self {
Order::Created { id } => format!("order {id}: created"),
Order::Paid { id, amount } => format!("order {id}: paid {amount:.2}"),
Order::Shipped { id, tracking } => format!("order {id}: shipped, tracking {tracking}"),
Order::Cancelled { id } => format!("order {id}: cancelled"),
}
}
fn cancel(self) -> Order {
match self {
Order::Created { id } | Order::Paid { id, .. } => Order::Cancelled { id },
other => other,
}
}
}
fn main() {
let o1 = Order::Created { id: 1 };
let o1 = o1.pay(49.90);
println!("{}", o1.status());
let o1 = o1.ship("ZX-5531");
println!("{}", o1.status());
let o1 = o1.cancel();
println!("{}", o1.status());
let o2 = Order::Created { id: 2 }.ship("AB-1000");
println!("{}", o2.status());
println!("{}", o2.cancel().status());
}
An order goes through the states created, paid, shipped, or cancelled.
fn ship(self, tracking: &str) -> Order {
match self {
Order::Paid { id, .. } => Order::Shipped { id, tracking: tracking.to_string() },
other => other,
}
}
The transition methods take self by value and return the new
state. Only a paid order can be shipped; in any other state the order is
returned unchanged by the other => other arm.
Order::Created { id } | Order::Paid { id, .. } => Order::Cancelled { id },
An order can be cancelled before it is shipped. Alternative patterns joined
with | must bind the same variables with the same types.
In the output, the shipped order ignores the cancellation and the unpaid order ignores the shipping request.
$ cargo run -q order 1: paid 49.90 order 1: shipped, tracking ZX-5531 order 1: shipped, tracking ZX-5531 order 2: created order 2: cancelled
Converting to and from enums
Conversions are added by implementing standard traits. Here we implement
TryFrom for parsing and Display for printing.
use std::fmt;
#[derive(Debug, Clone, Copy)]
enum Suit {
Clubs,
Diamonds,
Hearts,
Spades,
}
impl Suit {
const ALL: [Suit; 4] = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades];
}
impl TryFrom<char> for Suit {
type Error = String;
fn try_from(c: char) -> Result<Self, Self::Error> {
match c {
'C' => Ok(Suit::Clubs),
'D' => Ok(Suit::Diamonds),
'H' => Ok(Suit::Hearts),
'S' => Ok(Suit::Spades),
_ => Err(format!("unknown suit: {c}")),
}
}
}
impl fmt::Display for Suit {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let sym = match self {
Suit::Clubs => '♣',
Suit::Diamonds => '♦',
Suit::Hearts => '♥',
Suit::Spades => '♠',
};
write!(f, "{sym}")
}
}
fn main() {
for s in Suit::ALL {
println!("{:?} {}", s, s);
}
for c in ['H', 'S', 'X'] {
match Suit::try_from(c) {
Ok(s) => println!("{c} -> {s}"),
Err(e) => println!("{e}"),
}
}
}
The Suit enum represents the suits of playing cards.
const ALL: [Suit; 4] = [Suit::Clubs, Suit::Diamonds, Suit::Hearts, Suit::Spades];
Rust has no built-in way to iterate over the variants of an enum. A common solution is an associated constant that lists them.
impl TryFrom<char> for Suit {
type Error = String;
fn try_from(c: char) -> Result<Self, Self::Error> {
TryFrom is the trait for conversions that may fail. It returns a
Result; the error type is chosen by the implementation. The same
approach is used to convert integers to C-like enums.
match Suit::try_from(c) {
Ok(s) => println!("{c} -> {s}"),
Err(e) => println!("{e}"),
}
The conversion succeeds for valid letters and returns an error message for the others.
$ cargo run -q Clubs ♣ Diamonds ♦ Hearts ♥ Spades ♠ H -> ♥ S -> ♠ unknown suit: X
Option and Result
The standard library uses enums to represent optional values and errors.
fn divide(a: i32, b: i32) -> Option<i32> {
if b == 0 { None } else { Some(a / b) }
}
fn main() {
let results = [divide(10, 2), divide(7, 0), divide(-9, 3)];
for r in results {
match r {
Some(v) => println!("result: {v}"),
None => println!("division by zero"),
}
}
let n: Result<u8, _> = "300".parse::<u8>();
match n {
Ok(v) => println!("parsed {v}"),
Err(e) => println!("error: {e}"),
}
}
The Option and Result enums are defined in the
standard library roughly like this:
enum Option<T> {
None,
Some(T),
}
enum Result<T, E> {
Ok(T),
Err(E),
}
They are ordinary generic enums. Their variants are imported by the prelude,
so we write Some and Ok without a prefix. Because
None is a separate variant, Rust needs no null value: the
compiler makes us handle the missing case before we can use the value. More
on these types is in Rust Option type and
Rust error handling.
$ cargo run -q result: 5 division by zero result: -3 error: number too large to fit in target type
Source
The Rust Programming Language: Enums and Pattern Matching
The Rust Reference: Enumerations
In this article we have worked with enums in Rust. Patterns are covered in more detail in Rust match expression.
Author
List all Rust tutorials.