Hello! Welcome to the first lesson of our third module, "Essential Rust Concepts for Solana."
In the previous module, we built a solid foundation in Rust, ending with structs and impl blocks, which let us create custom data structures and define their behavior. Now, we're going to build on that by exploring some of Rust's most powerful features that are absolutely critical for writing safe and effective Solana programs.
This lesson focuses on enums, which are Rust's way of defining a type that can have one of several possible variations. You'll learn how to define enums, how to attach different kinds of data to their variants, and why they are so much more than just simple lists of constants you might have seen in other languages. Understanding enums is fundamental for working with Solana, as they are used everywhere—from defining program instructions to managing account states and handling errors.
What is an Enum?
If your experience is mainly with languages like JavaScript or TypeScript, you might think of an enum as a way to give friendly names to a set of numeric or string values. In Rust, enums are far more expressive. They are what's known as "sum types" or "tagged unions." This means an enum doesn't just represent a value from a fixed set; it defines a type that can be one of several different kinds of values, each potentially with its own unique data structure.
Let's start with a short video that introduces this core idea. It contrasts Rust's powerful enums with the simpler versions in other languages and highlights how they help you model data more safely by making invalid states impossible to represent in your code.
Rust Data Modelling Without Classes
Watch this segment from the video "Rust Data Modelling Without Classes" by No Boilerplate. It explains how Rust enums are 'algebraic sum types' and can model data with multiple possible forms.
Please watch from 00:39 to 01:34. Pay close attention to the example of the 'cat' and how the enum prevents an invalid state (a dead cat being hungry).
As the video explains, instead of having a single Cat struct with a boolean is_alive and an optional is_hungry field (which would require runtime checks to ensure a dead cat isn't hungry), you can define a Cat enum with Alive and Dead variants. Only the Alive variant would contain the is_hungry field, enforcing the logic at compile time.
Defining Your Own Enums
Let's dive into the syntax. You define an enum using the enum keyword, followed by the name of the enum and a list of its variants inside curly braces.
The simplest form of an enum has variants with no data associated with them. For a practical example, let's turn to the official Rust documentation.
Defining an Enum - The Rust Programming Language
The Rust Programming Language book provides a very clear introduction to defining enums. Let's read the first part of the chapter on enums.
Read the section "Defining an Enum" up to, but not including, the subsection "Enum Values". Focus on the IpAddrKind example.
This IpAddrKind enum is a perfect example of a simple enumeration. An IP address is either V4 or V6, but never both. The enum captures this exclusivity perfectly.
Attaching Data to Enum Variants
The real power of Rust enums comes from their ability to attach data directly to each variant. Furthermore, each variant can hold different types and amounts of data. This is what makes them "sum types" – the total set of possibilities for the enum is the sum of the possibilities of its variants.
Let's continue with the Rust book's IpAddr example, which shows this in action.
Defining an Enum - The Rust Programming Language
Now let's see how to add data to our enum variants. This part of the documentation shows how to embed data directly, making our types more concise and expressive.
Read from the subsection "Enum Values" up to the Message enum example (Listing 6-2). Notice how the enum IpAddr is redefined to hold the address data directly, and how the V4 and V6 variants can even hold different data types ((u8, u8, u8, u8) vs String).
As you saw, you can have variants that are:
- Unit-like: Variants with no associated data (e.g.,
IpAddrKind::V4). - Tuple-like: Variants with an unnamed tuple of data (e.g.,
IpAddr::V4(127, 0, 0, 1)). - Struct-like: Variants with named fields (e.g.,
Move { x: i32, y: i32 }which we will see next).
This video provides a great hands-on walkthrough of defining these different variant types.
The video "Rusty Enums" by Null Labs walks through defining an enum with different kinds of associated data, which should help solidify your understanding.
Watch from 00:53 to 05:14. This covers defining a basic enum, then adding tuple data, and finally adding struct-like data with named fields.
To summarize, here is a great visual example that shows the three kinds of variants in one enum definition.

This flexibility is heavily used in Solana programs. For instance, a program's instruction data is often deserialized into an enum where each variant represents a different action the program can perform, carrying the specific arguments needed for that action.
Test your understanding!
You're building a simple program for a blog. An action on a post can be one of the following:
- Publishing a post.
- Creating a draft, which includes the post's title as a
String. - Deleting a post, which requires the
post_idas au64. - Adding a comment, which includes the
post_id(u64) and the comment text (String).
Define a Rust enum called PostAction that can represent all of these possibilities.
Show answer
Here's one way to define the PostAction enum:
enum PostAction {
Publish,
CreateDraft(String),
Delete { post_id: u64 },
AddComment { post_id: u64, text: String },
}
Publishis a unit-like variant because no extra data is needed.CreateDraftis a tuple-like variant holding the title.Deleteis a struct-like variant holding thepost_id. We could have used a tupleDelete(u64), but giving the field a name makes the code more readable.AddCommentis also a struct-like variant, holding both the ID and the comment text.
Defining Behavior with impl
Just as we defined methods for structs in the last module using an impl block, we can do the exact same thing for enums. This allows us to group behavior related to the enum directly with its definition.
For example, we could add a method to our PostAction enum to describe what it does.
enum PostAction {
Publish,
CreateDraft(String),
Delete { post_id: u64 },
AddComment { post_id: u64, text: String },
}
impl PostAction {
fn describe(&self) {
// We'll learn how to inspect the `self` value
// and handle each variant in the lesson on pattern matching.
// For now, just know this is where the logic would go!
println!("This is a post action.");
}
}
fn main() {
let action = PostAction::Publish;
action.describe(); // Prints "This is a post action."
}
This ability to encapsulate both state (the variants) and behavior (the methods) makes enums a powerful tool for modeling complex systems.
A fantastic use case for this is creating a state machine. Since an enum can only be one of its variants at any given time, it's perfect for representing the state of an object and ensuring it can only transition between states in valid ways. Your interest in RPGs and console games makes this next example particularly relevant.
Rust Data Modelling Without Classes
Let's return to the "Rust Data Modelling Without Classes" video. This section demonstrates how to build a state machine using an enum to model the different power-up states of Mario.
Watch from 08:00 to 10:45. Focus on how the State enum defines all possible forms of Mario and how the collect method only allows valid transitions between those states. Don't worry about the details of the match expression for now; we'll cover that soon. The key idea is how the enum makes invalid states unrepresentable.
Conclusion
In this lesson, we've explored one of Rust's most defining features. Let's recap the key takeaways:
- Enums represent a type that can be one of several variants. Unlike enums in many other languages, they are not just named integers.
- Variants can contain data. This data can be in the form of tuples (unnamed fields) or structs (named fields), and each variant can have a different data structure.
- Enums enforce type safety. By defining all possible states of a value, you can leverage the Rust compiler to ensure you handle every case, making invalid states or transitions a compile-time error rather than a runtime bug.
- Enums can have methods. Using
implblocks, you can define behavior directly on your enum types, just like with structs.
This concept of creating custom types that precisely model your application's domain is central to writing safe and reliable code in Rust—and by extension, on Solana.
In our next lesson, we will look at a special and extremely common enum provided by Rust's standard library: Option. It is the primary tool for handling values that might be present or absent, providing a much safer alternative to the null or undefined values you might be used to from your front-end development background.
Can't find a good explanation? Sign up and we'll make it for you
Sign up