---
title: "11.2 Enums with data"
description: "Rust enum variants can carry values of their own, making an enum a value that is one of several shapes; the lightbulb feature that makes enums a headline of the language."
url: "https://learnrust.net/chapter-11/enums-with-data/"
last_updated: "2026-06-13"
---

# 11.2 Enums with data

The enums in lesson [11.1](https://learnrust.net/chapter-11/enums/) were lists of labels: `Red`, `Yellow`, `Green`. Useful, but not yet remarkable. This lesson adds the feature that makes Rust enums a headline of the language rather than a footnote: a variant can *carry data*, and different variants can carry different data. Once you see it, a lot of Rust clicks into place.

## A variant that holds a value

Consider modeling an IP address. There are two kinds, version 4 (four bytes, like `127.0.0.1`) and version 6 (a longer text form). A plain enum captures the *kind*:

```rust
enum IpKind {
    V4,
    V6,
}
```

But you also need the actual address, and the two kinds store it differently. In Rust, you attach the data directly to the variant:

```rust
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

fn main() {
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));
}
```

Look at what just happened. `IpAddr::V4` carries four `u8` values, like a tuple struct (lesson [10.4](https://learnrust.net/chapter-10/tuple-structs-and-unit-structs/)). `IpAddr::V6` carries a single `String`. They're variants of *the same type*, `IpAddr`, but they hold completely different data. A single value of type `IpAddr` is *either* four bytes *or* a string, and which one it is, is part of the value itself.

Creating a data-carrying variant looks like a function call: `IpAddr::V4(127, 0, 0, 1)`. That's not a coincidence; the variant name really does act like a function that builds the value. The kind and the data arrive together, inseparable, which is the whole point: you can't have a `V4` without its four bytes, and you can't accidentally pair a `V4` label with a string.

## Different variants, different shapes

Variants can each carry whatever shape suits them. They can hold nothing, a tuple of values, or named fields just like a struct. The Rust Book's `Message` enum shows all the forms in one type:

```rust
enum Message {
    Quit,                       // no data
    Move { x: i32, y: i32 },    // named fields, like a struct
    Write(String),              // a single String
    ChangeColor(i32, i32, i32), // three i32s, like a tuple
}
```

This one enum describes four genuinely different messages with four different payloads, under one type. The alternative without enums would be four separate structs and no single type that means "one of these four," or worse, one giant struct with a `kind` field and a pile of fields that are only meaningful for some kinds (the same impossible-states trap lesson [11.1](https://learnrust.net/chapter-11/enums/) warned about). The enum says exactly what's true: a `Message` is one of these four shapes, with the matching data, and nothing else.

> **Key insight**
>
> An enum with data is a value that is *one of several shapes*. This is the concept languages without it struggle to express cleanly: a function that returns "either a number or an error message," a list node that is "either a value-and-next or the end," a config setting that is "either on with a level, or off." Each is naturally one type with a few differently-shaped variants. Rust calls these *sum types*, and they're the partner to chapter 10's structs (*product types*): structs combine fields with "and", enums combine variants with "or".

## Getting the data back out

Putting data into a variant is only half the story; you need to get it out, and you can't reach in with a dot the way you would a struct field, because there's no single field that's always there. A `Message` might be a `Write` with a `String`, or a `Quit` with nothing. The only safe way to read the data is to first establish *which variant* you have, and that is exactly what `match` does. Matching on a data-carrying enum lets each arm name the data it carries:

```rust
fn describe(msg: &Message) -> String {
    match msg {
        Message::Quit => String::from("quit"),
        Message::Move { x, y } => format!("move to ({x}, {y})"),
        Message::Write(text) => format!("write: {text}"),
        Message::ChangeColor(r, g, b) => format!("color #{r:02x}{g:02x}{b:02x}"),
    }
}

fn main() {
    println!("{}", describe(&Message::Write(String::from("hi"))));
    println!("{}", describe(&Message::Move { x: 3, y: 4 }));
}
```

```
write: hi
move to (3, 4)
```

Each arm does two things at once: it checks which variant `msg` is, *and* it binds names to the data that variant carries, so the right side of the arm can use it. `Message::Write(text)` matches only the `Write` variant and names its `String` `text`. `Message::Move { x, y }` matches `Move` and names both fields. This binding is the heart of pattern matching, and it's so central that the next lesson is devoted to it. For now the takeaway is the partnership: enums carry data in, `match` takes it back out, and the compiler guarantees you've checked the variant before you touch the data, so you can never read a `String` out of a `Quit`.

## Upgrading the coin

Remember the `Coin` enum from lesson [11.1](https://learnrust.net/chapter-11/enums/)'s quiz? US quarters have a state design on the back, so a `Quarter` carries extra information the other coins don't. With a data-carrying variant, that's natural:

```rust
#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // ... and the other 48
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

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
        }
    }
}

fn main() {
    println!("{}", value_in_cents(&Coin::Quarter(UsState::Alaska)));
}
```

```
Quarter from Alaska!
25
```

Only `Quarter` carries a `UsState`; the other three variants carry nothing, and the `match` arm for `Quarter` pulls the state out and uses it. This is the Rust Book's example for good reason: it shows variants of one enum holding different amounts of data, and `match` handling each appropriately.

## Quiz time

**Question #1**

Define an enum `Shape` with a `Circle` variant carrying a radius (`f64`) and a `Rectangle` variant carrying a width and height (two `f64`s). Create one of each.

<details class="solution">
<summary>Show solution</summary>

```rust
enum Shape {
    Circle(f64),
    Rectangle(f64, f64),
}

fn main() {
    let c = Shape::Circle(2.0);
    let r = Shape::Rectangle(3.0, 4.0);
}
```

`Circle` carries one `f64`, `Rectangle` carries two. Both are values of type `Shape`. (You could also use named fields: `Rectangle { width: f64, height: f64 }`.)

</details>

**Question #2**

Why can't you read a variant's data with a dot, like `msg.text`, the way you read a struct field?

<details class="solution">
<summary>Show solution</summary>

Because an enum value isn't guaranteed to *have* that data. A `Message` might be `Write(String)` (has text) or `Quit` (has nothing), so `msg.text` would be meaningless when `msg` is `Quit`. You must first establish which variant you have, with `match`, and only the matching arm can name and use the carried data. This is the same safety the compiler enforces with exhaustiveness.

</details>

**Question #3**

Write a function `area(shape: &Shape) -> f64` for the `Shape` enum from question 1, using `match` to pull out each variant's data. (Use `3.14159` for pi.)

<details class="solution">
<summary>Show solution</summary>

```rust
fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle(radius) => 3.14159 * radius * radius,
        Shape::Rectangle(width, height) => width * height,
    }
}
```

The `Circle` arm binds `radius` to the carried `f64`; the `Rectangle` arm binds `width` and `height`. Each arm computes with exactly the data its variant holds, and the `match` is exhaustive (both variants covered), so it compiles.

</details>

There's one data-carrying enum so important that the standard library ships it and you've been using it without knowing. It exists to solve the single most expensive mistake in the history of programming languages. Next lesson: `Option`, and the end of null.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
