Structs are how you say "this thing has an X, and a Y, and a Z" — every field, every time, all at once. But plenty of real data doesn't work that way. A network address is either IPv4 or IPv6 — never both, never neither. A request either succeeded or it didn't. A search either found something or it came back empty. For data that's fundamentally "one of several possible shapes," Rust hands you a different tool entirely: the enum.
Defining an enum
IpAddrKind defines a brand-new type with exactly two possible values: IpAddrKind::V4 or IpAddrKind::V6 — and nothing else is possible, ever. Try to conjure up a third option and you get a compile error before your program runs even once. The type itself documents — and enforces — every shape this value could ever take.
The part that makes Rust's enums special: variants can carry data
Here's where Rust pulls noticeably ahead of "a fancy named integer," which is roughly what an enum amounts to in plenty of other languages. Each variant can carry its own data — and, more surprisingly, different variants of the same enum can carry completely different shapes of data:
V4 carries four separate u8s. V6 carries a single String. They're variants of the very same type, IpAddr, and yet they store fundamentally different things. Try to express this cleanly with a struct and you'd end up with optional fields that are sometimes meaningless, a comment somewhere explaining "only fill this one in if kind is V6," and a maintenance headache with your name on it. An enum makes that entire mess — what people sometimes call an "impossible state" — actually impossible to represent.
One enum, four completely different shapes of data
Push the idea a little further and you get something like this — a single type that's actually four different kinds of value, depending on which variant you're holding:
Sit with that for a second. Message is one type. Its four variants look like four entirely different kinds of values — one with no data whatsoever, one shaped like a struct, one wrapping a single value, one wrapping a tuple. Reach for four separate structs instead, and you'd lose the one thing that makes this powerful: a single type you can write a function like "handle any kind of message" against. The enum is that type — and it costs you nothing extra at runtime to get this flexibility.
Yes — methods work on enums too
impl blocks work on enums exactly like they do on structs, because under the hood, an enum is just another type the compiler knows about. You caught a glimpse of match back in control flow — here it is again, finally doing the job it was actually built for: looking at an enum value and asking, in one breath, "which variant is this, specifically — and what is it carrying?" The full lesson on everything match can do is immediately after this one. You're going to like it.
The most important enum in the standard library
Now for the one that genuinely changes how you write code from day one. Plenty of languages let any value be null — "nothing," "absent," "not loaded yet." It sounds convenient right up until it isn't: Tony Hoare, the computer scientist who introduced the null reference back in 1965, has since called it his "billion-dollar mistake," and it's hard to argue — an enormous fraction of all runtime crashes in software history trace back to exactly this one design decision.
Rust doesn't have null. Not anywhere, not in any form. Instead, the standard library defines an enum that represents "a value that might or might not be present" — and it's so fundamental to how the language works that it's quite literally part of the prelude: available everywhere, with no import, ever:
That's the entire definition. An Option<T> is either Some, wrapping a value of some type T, or None, wrapping nothing at all. The angle-bracket <T> makes it generic — the same definition works for an Option<i32>, an Option<String>, an Option<Rectangle>, anything — and we'll meet that <T> syntax properly in the generics lesson.
Here's the part that actually prevents bugs, though, and it's wonderfully simple: you cannot use the value inside an Option<T> as though it were a plain T:
The compiler stops you cold, because Option<i8> and i8 are simply different types — full stop — and that difference is the entire safety mechanism. In a language with null, y could secretly be "nothing," and you'd find out only once your program crashed trying to use it. In Rust, the type itself — sitting right there in the signature, impossible to scroll past — announces "this might not have a value, so you have to deal with that possibility before you're allowed anywhere near what's inside." The compiler will not let you forget. That one guarantee quietly wipes out an enormous share of the most common bugs in all of software.
So how do you actually get the value back out, given that you can't just treat it like a normal T? You ask the enum which variant it is — which means you're back to match again. And this time, we go all the way in.
Quick exercise
Define an enum Shape with three variants: Circle(f64) (storing a radius), Rectangle(f64, f64) (width and height), and Triangle(f64, f64) (base and height). Don't compute anything with them yet — just get the definition compiling, build one value of each variant inside main, derive Debug, and print all three. You'll teach this enum to compute its own area in the very next lesson, once you actually have the tool for it.
That tool is pattern matching — the feature that turns an enum from "a type that can hold one of several things" into "a type you can do something genuinely satisfying with." It's one of Rust's best ideas, and it's next.