Enums
An enum (short for enumeration) is a type whose value is exactly one of a fixed set of named variants. Where a struct bundles fields together so you always have all of them at once, an enum represents a choice: a Direction is North, South, East, or West, never more than one at a time. Enums matter in Rust because they let you model real-world “one of these” situations directly in the type system, and because two of the most important types in the language — Option<T> and Result<T, E> — are themselves enums. Once you understand user-defined enums, you already understand how Rust represents “maybe a value” and “success or failure” without needing null or exceptions.
Overview: What Enums Are and How They Work
Think of an enum value as a small labeled box. The label says which variant is inside (its “discriminant”, or tag), and the box has just enough room to hold whatever data that particular variant carries. When you create Message::Write(String::from("hi")), Rust builds one value tagged “this is a Write” with a String stored alongside that tag. A Message::Quit value is tagged “this is a Quit” and carries no extra data at all. Crucially, at any moment a Message value is only ever one of these variants — never a mix, and never “none of the above.” This is why enums are called sum types: the set of possible values is the sum (the union) of the possible values of each variant. A struct, by contrast, is a product type — a struct value must supply all of its fields simultaneously.
Under the hood, the compiler lays out an enum as a discriminant (usually a small integer) plus enough space for the largest variant’s payload. All variants of the same enum share this space, similar to a C union but tag-checked by the compiler, which is why an enum’s size is roughly the size of its biggest variant, not the sum of every variant’s size. Rust can sometimes eliminate the discriminant entirely when a type’s own bit pattern has “spare” values — Option<&T> is famously the same size as a plain reference, because a null pointer bit pattern doubles as None — but this is an optimization the compiler applies automatically; you don’t need to design for it.
The tool you’ll use to look inside an enum is the match expression (or its lighter cousin, if let). Unlike a chain of if/else if, a match on an enum must cover every variant — if you add a new variant later and forget to update a match elsewhere, the compiler stops you at every call site. This exhaustiveness check is one of Rust’s best refactoring safety nets. Matching also interacts directly with ownership: matching a value by value can move data out of it, while matching a reference (&my_enum) only borrows it, thanks to a feature called match ergonomics that automatically types the bindings inside match arms as references. We’ll see both styles below, and Common Mistakes will show what happens when you mix them up.
Syntax
An enum is declared with the enum keyword, a name, and a set of variants inside braces. Each variant can be unit-like (no data), tuple-like (unnamed positional data), or struct-like (named fields):
enum EnumName {
UnitVariant,
TupleVariant(Type1, Type2),
StructVariant { field1: Type1, field2: Type2 },
}
| Variant kind | Example | When to use |
|---|---|---|
| Unit-like | Quit |
No extra data — just a named case |
| Tuple-like | Write(String) |
One or more unnamed values, like a lightweight tuple |
| Struct-like | Move { x: i32, y: i32 } |
Named fields, for clarity when a variant carries several values |
To inspect an enum value, use match: a keyword, the value being matched, and a block of pattern => expression arms separated by commas. Every possible variant must appear as a pattern (or be covered by a catch-all _ arm), and every arm must produce the same type, since the whole match is itself an expression that evaluates to a value.
Examples
Example 1: A Simple Enum and Matching
The simplest enums are unit-like: no variant carries any data, so the enum is essentially a fixed list of named states — useful anywhere you’d reach for a boolean flag or an integer code in other languages, but safer, because invalid values are unrepresentable.
enum Direction {
North,
South,
East,
West,
}
fn describe(dir: &Direction) -> &str {
match dir {
Direction::North => "You are heading north.",
Direction::South => "You are heading south.",
Direction::East => "You are heading east.",
Direction::West => "You are heading west.",
}
}
fn main() {
let heading = Direction::North;
println!("{}", describe(&heading));
let opposite = match heading {
Direction::North => Direction::South,
Direction::South => Direction::North,
Direction::East => Direction::West,
Direction::West => Direction::East,
};
println!("{}", describe(&opposite));
}
Output:
You are heading north.
You are heading south.
describe borrows a &Direction and returns a string slice describing it. In main, heading is created and immediately borrowed for the first println!; nothing is moved yet. The second match, match heading (matching by value, not by reference), does move heading into the match — that’s fine because heading is never used again afterward. Each arm returns a new Direction, which match hands back as the value of opposite.
Example 2: Enums That Carry Data
Real-world enums usually carry data, and different variants can carry completely different data — something a single struct can’t express as cleanly. Message below has one variant of each kind: unit, struct-like, and two tuple-like variants.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn process(msg: &Message) {
match msg {
Message::Quit => println!("Quit received"),
Message::Move { x, y } => println!("Move to ({}, {})", x, y),
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to RGB({}, {}, {})", r, g, b),
}
}
fn main() {
let messages = vec![
Message::Quit,
Message::Move { x: 10, y: 20 },
Message::Write(String::from("hello")),
Message::ChangeColor(255, 0, 128),
];
for msg in &messages {
process(msg);
}
}
Output:
Quit received
Move to (10, 20)
Text message: hello
Change color to RGB(255, 0, 128)
process takes a &Message. Because we match on a reference, match ergonomics automatically type x, y, text, r, g, and b as references to their underlying fields rather than moving them out — so messages, and the individual Message values it owns, stay intact and could be reused after the loop.
Example 3: Methods on Enums with impl
Just like structs, enums can have methods defined in an impl block, and a match inside a method is the standard way to compute something that depends on which variant self is.
enum Shape {
Circle(f64),
Rectangle(f64, f64),
Triangle(f64, f64),
}
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(base, height) => 0.5 * base * height,
}
}
fn name(&self) -> &str {
match self {
Shape::Circle(_) => "circle",
Shape::Rectangle(_, _) => "rectangle",
Shape::Triangle(_, _) => "triangle",
}
}
}
fn main() {
let shapes = vec![
Shape::Circle(2.0),
Shape::Rectangle(3.0, 4.0),
Shape::Triangle(6.0, 2.0),
];
for shape in &shapes {
println!("{} area: {:.2}", shape.name(), shape.area());
}
}
Output:
circle area: 12.57
rectangle area: 12.00
triangle area: 6.00
Both area and name take &self, so calling shape.area() only borrows the shape. Inside the match, patterns like Shape::Circle(radius) bind radius as a &f64 (again, match ergonomics), which is why the arithmetic works directly against a borrowed field without any manual dereferencing.
Example 4: Enums Combined with Option
Enums pair naturally with Option<T>, the standard-library enum Rust uses instead of null. A function that might not be able to produce a value returns Option<T>, and the caller is forced by the type system to handle both the Some and None cases.
enum Role {
Admin,
Editor,
Viewer,
}
fn parse_role(input: &str) -> Option<Role> {
match input {
"admin" => Some(Role::Admin),
"editor" => Some(Role::Editor),
"viewer" => Some(Role::Viewer),
_ => None,
}
}
fn main() {
let inputs = ["admin", "guest", "viewer"];
for input in inputs {
match parse_role(input) {
Some(Role::Admin) => println!("{}: full access granted", input),
Some(Role::Editor) => println!("{}: can edit content", input),
Some(Role::Viewer) => println!("{}: read-only access", input),
None => println!("{}: unknown role", input),
}
}
}
Output:
admin: full access granted
guest: unknown role
viewer: read-only access
parse_role returns Option<Role> — Some(Role::...) for recognized input, None otherwise. Because the return type is an Option, main can’t accidentally use a Role that doesn’t exist; the nested pattern Some(Role::Admin) matches only when parsing succeeded and produced that specific variant, and the None arm handles the unrecognized case explicitly instead of crashing.
How Enums Work Step by Step
By default the compiler assigns discriminant values itself, but for field-less enums (every variant is unit-like) you can specify them explicitly and even cast a value to its underlying integer with as. This is common when an enum mirrors an external format such as HTTP status codes:
enum HttpStatus {
Ok = 200,
NotFound = 404,
ServerError = 500,
}
fn main() {
let status = HttpStatus::NotFound;
println!("Status code: {}", status as i32);
}
Output:
Status code: 404
- The compiler assigns
HttpStatus::Okthe discriminant 200,NotFound404, andServerError500, exactly as written. HttpStatus::NotFoundis built as a single integer-sized value tagged 404 — no heap allocation, since none of the variants carry data.status as i32reads that discriminant directly and converts it to ani32.println!formats the resulting integer as text.
Separately, every time the compiler sees a match on an enum anywhere in your program, it walks the enum’s variant list and verifies the match’s patterns cover all of them (or that a wildcard _ arm is present). This check happens purely at compile time and costs nothing at runtime — it’s the compiler reading your code, not your program checking itself while running.
Common Mistakes
Mistake 1: A Non-Exhaustive match
It’s easy to add a variant to an enum and forget to update every match on it. The compiler won’t let this compile:
enum TrafficLight {
Red,
Yellow,
Green,
}
fn advice(light: TrafficLight) -> &'static str {
match light {
TrafficLight::Red => "Stop",
TrafficLight::Green => "Go",
}
}
Rust rejects this with a “non-exhaustive patterns” error because Yellow isn’t handled — match must cover every variant. Add the missing arm:
enum TrafficLight {
Red,
Yellow,
Green,
}
fn advice(light: &TrafficLight) -> &'static str {
match light {
TrafficLight::Red => "Stop",
TrafficLight::Yellow => "Slow down",
TrafficLight::Green => "Go",
}
}
fn main() {
let light = TrafficLight::Yellow;
println!("{}", advice(&light));
}
Output:
Slow down
Mistake 2: Using a Value After match Moves It
Matching a value by value (match msg { ... }, not match &msg { ... }) moves it into the match, just like passing it to a function by value. If the pattern would take ownership of non-Copy data, the whole value is considered moved — you can’t match on it again:
enum Message {
Write(String),
}
fn main() {
let msg = Message::Write(String::from("hello"));
match msg {
Message::Write(text) => println!("{}", text),
}
// msg was moved into the match above, so this second match fails to compile:
match msg {
Message::Write(text) => println!("{}", text),
}
}
The second match msg tries to use msg again, but it was already moved into the first match. The compiler reports “use of moved value: msg“. The fix is to match on a reference so the match only borrows:
enum Message {
Write(String),
}
fn main() {
let msg = Message::Write(String::from("hello"));
match &msg {
Message::Write(text) => println!("{}", text),
}
match &msg {
Message::Write(text) => println!("Still available: {}", text),
}
}
Output:
hello
Still available: hello
Mistake 3: Comparing Enum Values Without Deriving PartialEq
Unlike some languages, Rust doesn’t give every type == for free — an enum only supports == if it implements the PartialEq trait:
enum Status {
Active,
Inactive,
}
fn main() {
let s = Status::Active;
if s == Status::Active {
println!("Active!");
}
}
This fails with “binary operation == cannot be applied to type Status” because Status has no PartialEq implementation. Fix it by deriving it:
#[derive(PartialEq)]
enum Status {
Active,
Inactive,
}
fn main() {
let s = Status::Active;
if s == Status::Active {
println!("Active!");
}
}
Output:
Active!
#[derive(PartialEq)] generates a variant-by-variant equality check automatically, letting == work as expected.
Best Practices
- Prefer
matchover longif/elsechains when branching on an enum — you get exhaustiveness checking for free. - Match on a reference (
&value) whenever you only need to read the data; match on the value itself only when you intend to consume or move it. - Reach for
Option<T>instead of a sentinel value (-1, an empty string, and so on) to represent “no value” — the compiler then forces every caller to handle the absent case. - Derive common traits (
Debug,PartialEq,Clone) on your enums up front; they’re cheap to add and frequently needed for testing, comparisons, and debugging. - Prefer a struct-like variant with named fields over a tuple-like variant once a variant carries more than two or three payload values, for readability.
- Use a catch-all
_arm sparingly — it silently accepts future variants without a compile error, which defeats exhaustiveness checking’s main benefit.
Practice Exercises
- Define an enum
Coinwith variantsPenny,Nickel,Dime, andQuarter. Write a functionvalue_in_centsthat takes a&Coinand returns its value as au32(1, 5, 10, 25) usingmatch. Print the total value of aVec<Coin>containing one of each. Expected output:41. - Define an enum
WebEventwith a unit variantPageLoad, a tuple variantClick(i32, i32)holding coordinates, and a struct-like variantKeyPress { key: char }. Write a function that matches on a&WebEventand prints a description for each. Test it with one value of each variant. - Write a function
safe_divide(a: f64, b: f64) -> Option<f64>that returnsNonewhenbis0.0andSome(a / b)otherwise. Call it with a few pairs of inputs, including one that divides by zero, and usematchto print either the result or a “cannot divide by zero” message.
Summary
- An enum defines a type whose value is exactly one of a fixed set of named variants — a sum type, as opposed to a struct’s product type.
- Variants can be unit-like, tuple-like, or struct-like, and different variants of the same enum can carry completely different data.
matchmust handle every variant (or include a_catch-all); the compiler enforces this at compile time, making enums safe to extend and refactor.- Matching on a reference (
&value) borrows the data via match ergonomics; matching on the value itself can move it, so a moved enum can’t be matched again. Option<T>andResult<T, E>are ordinary enums from the standard library — once you understand your own enums, you already understand how Rust models absence and failure.- Field-less enums can specify explicit discriminants and be cast with
asto their underlying integer type.
