Syntax Cache
BlogMethodFeaturesHow It WorksBuild a Game
  1. Home
  2. Rust
  3. Rust Structs & Enums Practice: Custom Types, Option & Result
Rust37 exercises

Rust Structs & Enums Practice: Custom Types, Option & Result

Master Rust custom types including structs, enums, Option, and Result. Practice type definitions and pattern matching on variants.

Common ErrorsQuick ReferencePractice
Warm-up1 / 2

Can you write this from memory?

Define a struct named `Point` with two `i32` fields: `x` and `y`.

On this page
  1. 1Compiler Error E0616: Field Is Private
  2. 2Compiler Error E0004: Non-Exhaustive Patterns
  3. 3Compiler Error E0282: Type Annotations Needed for None
  4. 4Struct or Enum? The Decision Rule
  5. 5Struct Definitions and impl Blocks
  6. Field Init Shorthand
  7. Struct Update Syntax
  8. 6Enum Variants: Unit, Tuple, and Struct
  9. 7Option and Result Are Just Enums
  10. Methods You Will Use Daily
  11. 8Further Reading
Compiler Error E0616: Field Is PrivateCompiler Error E0004: Non-Exhaustive PatternsCompiler Error E0282: Type Annotations Needed for NoneStruct or Enum? The Decision RuleStruct Definitions and impl BlocksEnum Variants: Unit, Tuple, and StructOption and Result Are Just EnumsFurther Reading

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.

Related Rust Topics
Rust Foundations: Variables, Types & Basic SyntaxRust Pattern Matching: match, if let, DestructuringRust Error Handling: Result, Option, ? Operator

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.

Ready to practice?

Start practicing Rust Structs & Enums: Custom Types, Option & Result with spaced repetition

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). Enum = one of several variants (OR). If a value can be in multiple states, use an enum. If it always has the same set of fields, use a struct.

  • 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.

Each enum variant can carry different data: no data (unit), unnamed data (tuple), or named fields (struct). You must pattern-match to access the data inside—no direct field access.

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> replaces null (Some(T) or None). Result<T, E> replaces exceptions (Ok(T) or Err(E)). The compiler forces you to handle both cases—no silent null pointer bugs.

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

MethodOnDoes
unwrap_or(default)Option/ResultReturns value or default
map(|x| ...)Option/ResultTransforms the inner value
is_some() / is_ok()Option/ResultChecks variant without consuming
ok_or("msg")OptionConverts Option to Result
as_deref()Option<String>Converts to Option<&str>
map_err(|e| ...)ResultTransforms 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

Prompt

How does Rust handle null values?

What a strong answer looks like

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

Define structs with named fields, tuple structs, unit structsUse field init shorthand and struct update syntaxAdd methods and constructors with impl blocksDefine enums with unit, tuple, and struct variantsUse Option<T> and Result<T, E> with common combinatorsMatch on enum variants exhaustivelyChoose between pub fields vs private fields with constructorsFix E0616, E0282, E0004

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 definition
struct User {
    username: String,
    email: String,
    active: bool,
}
Tuple struct
struct Point(i32, i32, i32);
Unit struct
struct Marker;
Struct instantiation
let user = User {
    username: String::from("alice"),
    email: String::from("alice@example.com"),
    active: true,
};
Field init shorthand
let name = String::from("Ada");
let u = User { name, active: true };
Struct update syntax
let u2 = User { email: new_email, ..u1 };
Impl block
impl User {
    fn new(name: String) -> Self {
        Self { name, active: true }
    }
    fn is_active(&self) -> bool {
        self.active
    }
}
Enum definition
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}
Option usage
let some_number: Option<i32> = Some(5);
let no_number: Option<i32> = None;
Result usage
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

Example 1Difficulty: 2/5

Fill in the missing field to construct the Point with `y` equal to 4.

y: 4
Example 2Difficulty: 3/5

Fill in the struct update syntax to reuse remaining fields.

..
Example 3Difficulty: 3/5

Fill in the operator to add 1 to value.

+=

+ 34 more exercises

Quick Reference
Rust Structs & Enums: Custom Types, Option & Result Cheat Sheet →

Copy-ready syntax examples for quick lookup

Further Reading

  • Rust Newtype Pattern: Catch Unit Bugs at Compile Time18 min read

Related Design Patterns

Builder PatternFactory Pattern

Start practicing Rust Structs & Enums: Custom Types, Option & Result

Free daily exercises with spaced repetition. No credit card required.

← Back to Rust Syntax Practice
Syntax Cache

Build syntax muscle memory with spaced repetition.

Product

  • Pricing
  • Our Method
  • Daily Practice
  • Design Patterns
  • Interview Prep

Resources

  • Blog
  • Compare
  • Cheat Sheets
  • Vibe Coding
  • Muscle Memory

Languages

  • Python
  • JavaScript
  • TypeScript
  • Rust
  • SQL
  • GDScript

Legal

  • Terms
  • Privacy
  • Contact

© 2026 Syntax Cache

Cancel anytime in 2 clicks. Keep access until the end of your billing period.

No refunds for partial billing periods.