Can you write this from memory?
Define a struct named `Point` with two `i32` fields: `x` and `y`.
Structs and enums are the building blocks of Rust's type system. Structs let you group related data; enums let you define types that can be one of several variants.
Two enums are so important they're in the prelude: Option<T> (for nullable values) and Result<T, E> (for operations that might fail). You'll use them constantly.
This page focuses on the syntax for defining and using custom types. From basic struct definitions to enum variants with data, until the patterns are automatic.
Struct fields are private by default. Making the struct pub only makes the type visible — each field needs its own pub:
mod user {
pub struct User {
name: String, // private!
}
}
// WRONG: can see the type, but not the field
// let u = user::User { name: String::from("Ada") };
// error[E0616]: field `name` of struct `User` is private
Two fixes, depending on your design:
// Option 1: make fields pub (simple data types)
mod user_pub {
pub struct User {
pub name: String,
}
}
// Option 2: keep fields private, expose a constructor (preserves invariants)
mod user_new {
pub struct User {
name: String,
}
impl User {
pub fn new(name: impl Into<String>) -> Self {
Self { name: name.into() }
}
pub fn name(&self) -> &str { &self.name }
}
}
Prefer Option 2 when the struct has invariants to enforce (e.g., validated email, non-empty name). For complex construction logic, the Builder pattern provides a fluent API that validates fields step by step.
Your match doesn't cover every enum variant:
enum Status { Active, Inactive, Pending }
// WRONG: missing Pending
// match status {
// Status::Active => "on",
// Status::Inactive => "off",
// // error[E0004]: non-exhaustive patterns: `Pending` not covered
// }
// RIGHT: handle all variants
match status {
Status::Active => "on",
Status::Inactive => "off",
Status::Pending => "waiting",
}
// Or use _ as a catch-all (careful: won't warn on new variants)
match status {
Status::Active => "on",
_ => "not active",
}
The compiler cannot infer the type of None without context:
// WRONG: what type is this Option?
// let x = None; // error[E0282]: type annotations needed
// RIGHT: annotate the binding
let x: Option<i32> = None;
// RIGHT: turbofish
let x = Option::<i32>::None;
- Struct: all fields exist at once (AND). A user always has a name AND an age.
- Enum: one of several variants (OR). A message is Quit OR Write OR Move.
If a value can be in multiple states, use an enum. If it always has the same set of fields, use a struct.
struct User {
name: String,
email: String,
active: bool,
}
impl User {
// Constructor (associated function — no self)
fn new(name: String, email: String) -> Self {
Self { name, email, active: true } // field init shorthand
}
// Method (takes &self)
fn is_active(&self) -> bool {
self.active
}
// Mutable method (takes &mut self)
fn deactivate(&mut self) {
self.active = false;
}
}
Field Init Shorthand
When a variable has the same name as a struct field, you can omit the field name:
let name = String::from("Ada");
let email = String::from("ada@example.com");
// Long form
let u = User { name: name, email: email, active: true };
// Shorthand — same result
let u = User { name, email, active: true };
Struct Update Syntax
Create a new struct from an existing one, overriding specific fields:
let user1 = User::new("Ada".into(), "ada@example.com".into());
let user2 = User { email: String::from("new@example.com"), ..user1 };
// user1.name was MOVED into user2 (String is not Copy)
// user1 can no longer be used as a whole
The ..user1 syntax moves non-Copy fields. After the update, the original struct cannot be used if any moved field was consumed. Tuple structs that wrap a single type are known as newtypes -- a powerful pattern for adding type safety. For a deep dive, read how Rust newtypes catch unit bugs at compile time. When you need to construct different variants of a type at runtime, the Factory pattern formalizes this approach.
enum Message {
Quit, // unit variant (no data)
Write(String), // tuple variant (unnamed data)
Move { x: i32, y: i32 }, // struct variant (named fields)
}
let msg = Message::Write(String::from("hi"));
match msg {
Message::Quit => println!("quit"),
Message::Write(text) => println!("{text}"),
Message::Move { x, y } => println!("move to ({x}, {y})"),
}
You must pattern-match to access data inside enum variants. Trying to access it directly won't compile. The pattern matching guide covers all the ways to destructure enum data. For a quick-reference of enum syntax, see the enums cheat sheet.
// Option<T>: Some(T) or None
let value: Option<i32> = Some(5);
let empty: Option<i32> = None;
// Result<T, E>: Ok(T) or Err(E)
let ok: Result<i32, String> = Ok(1);
let err: Result<i32, String> = Err(String::from("fail"));
Methods You Will Use Daily
| Method | On | Does |
|---|---|---|
unwrap_or(default) | Option/Result | Returns value or default |
map(|x| ...) | Option/Result | Transforms the inner value |
is_some() / is_ok() | Option/Result | Checks variant without consuming |
ok_or("msg") | Option | Converts Option to Result |
as_deref() | Option<String> | Converts to Option<&str> |
map_err(|e| ...) | Result | Transforms the error |
- The Rust Book: Structs — definitions, methods, update syntax
- The Rust Book: Enums — variants, Option, match
- std::option::Option — full combinator API
- std::result::Result — map, map_err, and_then
When to Use Rust Structs & Enums: Custom Types, Option & Result
- Use structs to group related data with named fields.
- Use tuple structs for lightweight types where field names are unnecessary.
- Use enums when a value can be one of several distinct variants.
- Use `Option<T>` instead of null. The compiler forces you to handle the missing case.
- Use `Result<T, E>` for operations that can fail. The compiler forces error handling.
Check Your Understanding: Rust Structs & Enums: Custom Types, Option & Result
How does Rust handle null values?
Rust has no null. Instead, `Option<T>` represents values that might be absent: `Some(value)` when present, `None` when absent. The compiler forces you to handle both cases, preventing null pointer exceptions.
What You'll Practice: Rust Structs & Enums: Custom Types, Option & Result
Common Rust Structs & Enums: Custom Types, Option & Result Pitfalls
- You get `error[E0616]: field is private` -- struct fields are private by default, even if the struct is `pub`. Either add `pub` to fields or provide a constructor with `pub fn new()`.
- You get `error[E0282]: type annotations needed` when writing `None` -- the compiler cannot infer the type. Add an annotation: `let x: Option<i32> = None;`.
- You get `error[E0004]: non-exhaustive patterns` -- your `match` doesn't cover all variants. Add the missing arms or use `_ => ...` as a catch-all. Note: `_` won't warn you when new variants are added later.
- You use struct update syntax `..user1` and then `user1` is unusable -- `..` moves non-Copy fields (like String). If you need the original, clone the fields or the whole struct first.
- You try to access enum data like a struct field (`msg.0` or `msg.text`) -- enum variant data can only be accessed through pattern matching (`match` or `if let`).
- You get `error[E0308]: expected String, found &str` -- struct fields typed as `String` need owned strings. Use `String::from("...")` or `"...".to_string()`, not a bare literal.
Rust Structs & Enums: Custom Types, Option & Result FAQ
What is the difference between a struct and an enum?
A struct holds multiple pieces of data simultaneously (AND). An enum is one of several variants (OR). Use structs for grouping data, enums for representing choices or states.
When should I use a tuple struct vs a regular struct?
Tuple structs are good for simple wrappers or when position is more meaningful than names: `struct Point(i32, i32)`. Use regular structs when field names add clarity.
Why use Option instead of checking for null?
Option is part of the type system, so the compiler knows when a value might be absent. You can't accidentally use a None value without handling it. No more null pointer exceptions.
What is a unit struct?
A struct with no fields: `struct Marker;`. Used for traits, type-level markers, or when you need a type but no data. Takes zero bytes.
Can enum variants have different types of data?
Yes! Each variant can have no data, unnamed data (tuple-like), or named data (struct-like): `enum Message { Quit, Move { x: i32, y: i32 }, Write(String) }`.
What is field init shorthand?
When a variable has the same name as a struct field, you can omit the colon: `User { name, email, active: true }` instead of `User { name: name, email: email, active: true }`.
Does struct update syntax (..) clone or move?
It moves non-Copy fields. After `let u2 = User { email: new_email, ..u1 }`, u1 is partially moved if any non-Copy field (like String) was consumed. Copy fields are copied.
Rust Structs & Enums: Custom Types, Option & Result Syntax Quick Reference
struct User {
username: String,
email: String,
active: bool,
}struct Point(i32, i32, i32);struct Marker;let user = User {
username: String::from("alice"),
email: String::from("alice@example.com"),
active: true,
};let name = String::from("Ada");
let u = User { name, active: true };let u2 = User { email: new_email, ..u1 };impl User {
fn new(name: String) -> Self {
Self { name, active: true }
}
fn is_active(&self) -> bool {
self.active
}
}enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("division by zero"))
} else {
Ok(a / b)
}
}Rust Structs & Enums: Custom Types, Option & Result Sample Exercises
Fill in the missing field to construct the Point with `y` equal to 4.
y: 4Fill in the struct update syntax to reuse remaining fields.
..Fill in the operator to add 1 to value.
+=+ 34 more exercises
Copy-ready syntax examples for quick lookup