Skip to content

Rust Data Modeling: Structs, Enums & Robust Error Handling

Created: 2026-11-06 Last updated: 2026-11-07

Executive Summary: Classical object-oriented languages rely on class hierarchies, inheritance, runtime polymorphism, and unchecked exceptions—often obscuring control flow and resulting in pervasive NullPointerExceptions or unhandled runtime failures. Rust fundamentally rejects this paradigm. Instead of inheritance, Rust adopts composition over inheritance powered by Algebraic Data Types (ADTs): Structs (Product Types) that group related data with deterministic memory layouts, and Enums (Sum Types) that model mutually exclusive states with rich payloads. Furthermore, Rust eliminates null references entirely via Option<T> and models fallible operations explicitly through Result<T, E>. This chapter forms Part 1.2 (Fundamentals): deconstructing struct variants and memory alignment, implementing methods and associated constructors, mastering exhaustive pattern matching, engineering robust error propagation with the ? operator, and deploying an industrial-grade, zero-panic web access log parser.

  • Structs & Memory Layout


    Construct classic named structs, tuple structs (Newtype pattern), and zero-sized unit structs with strict cache-line alignment.

  • Methods & impl Blocks


    Implement instance methods using &self, &mut self, and consuming self, alongside associated constructor functions (Self::new).

  • Algebraic Enums & Pattern Matching


    Model complex state machines with payload-bearing sum types, exhaustive match expressions, match guards, and modern let else syntax.

  • Option<T>, Result<T, E> & ?


    Banish null pointer bugs forever, distinguish recoverable errors from unrecoverable panics, and propagate errors cleanly with the ? operator.


1. Structs: Engineering Product Types

In type theory, a Product Type represents a composite data type whose state space is the Cartesian product of its constituent fields. In Rust, product types are instantiated via struct. Rust provides three distinct struct variations tailored for different architectural scenarios:

  1. Classic Named Structs: Explicit field names for clarity and self-documenting APIs.
  2. Tuple Structs: Positional fields without explicit names, ideal for lightweight wrappers and the Newtype pattern.
  3. Unit-Like Structs: Structs with zero fields, occupying zero bytes in memory (Zero-Sized Types / ZST), ideal for trait markers and compile-time state machines.
Declaring the Three Struct Variants
// 1. Classic Named Field Struct
#[derive(Debug, Clone, PartialEq)]
pub struct NetworkInterface {
    pub name: String,
    pub mtu: u32,
    pub is_up: bool,
}

// 2. Tuple Struct (Newtype Pattern for Domain Type Safety)
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Port(pub u16);

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Megabytes(pub f64);

// 3. Unit-Like Struct (Zero-Sized Type: occupies 0 bytes on the stack!)
#[derive(Debug, Copy, Clone)]
pub struct PromiscuousModeMarker;

Memory Alignment and Struct Padding

By default, the Rust compiler (rustc) uses the repr(Rust) representation. Unlike C, where fields are laid out strictly in declaration order, rustc reserves the right to reorder struct fields to minimize CPU padding and optimize memory alignment.

Memory Layout Optimization (Padding Elimination)
Unoptimized C declaration order (40 bytes total due to alignment holes):
[ u8: 1 byte ] [ 7 bytes padding ] [ u64: 8 bytes ] [ bool: 1 byte ] [ 7 bytes padding ] [ String: 24 bytes ]

Optimized rustc field reordering (32 bytes total):
[ String: 24 bytes ] [ u64: 8 bytes ] [ u8: 1 byte ] [ bool: 1 byte ] [ 6 bytes trailing padding ]

[!TIP] For direct C-FFI or hardware memory-mapped I/O (MMIO), apply #[repr(C)] or #[repr(packed)] to enforce deterministic, non-reordered byte structures matching external specifications.


2. Methods and Associated Functions (impl)

Behavior in Rust is completely separated from data declarations. Methods and associated functions are defined inside impl blocks.

Receiver Semantics: &self, &mut self, and self

The parameter in the first position of a method signature determines its borrowing and ownership contract:

Receiver Signature Ownership / Borrow Semantics Typical Use Case
&self Shared Read-Only Borrow: Multiple callers can invoke concurrently. Getters, inspection, serialization, queries.
&mut self Exclusive Mutable Borrow: Exactly one caller has exclusive write access. Mutating internal state, buffers, counters.
self Ownership Move (Consumption): The instance is destroyed or transformed upon exit. Builder pattern finalizers, state machine transitions.
Comprehensive impl Block Implementation
pub struct LinuxService {
    name: String,
    pid: Option<u32>,
    restart_count: u32,
}

impl LinuxService {
    /// Associated Constructor Function (No self parameter, invoked via LinuxService::new)
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            pid: None,
            restart_count: 0,
        }
    }

    /// Shared Borrow (&self): Read-only query
    pub fn status(&self) -> (&str, bool) {
        (&self.name, self.pid.is_some())
    }

    /// Mutable Borrow (&mut self): Modifies internal state
    pub fn start(&mut self, assigned_pid: u32) {
        self.pid = Some(assigned_pid);
        self.restart_count += 1;
    }

    /// Consuming Receiver (self): Transitions state and consumes the original instance
    pub fn decommission(self) -> String {
        format!("Service '{}' has been permanently decommissioned.", self.name)
        // 'self' is dropped and deallocated here at the end of the scope
    }
}

3. Algebraic Enums: Modeling Mutually Exclusive States

In many mainstream languages, an enum is merely a glorified integer constant. In Rust, an enum is a full Sum Type (or Tagged Union / Discriminated Union). Each variant can carry distinct payloads of arbitrary types and sizes.

Rich Algebraic Enum with Diverse Payloads
#[derive(Debug, Clone)]
pub enum NetworkEvent {
    /// Unit variant: No payload
    LinkDown,
    /// Tuple variant: Carries IP and Port
    ConnectionEstablished(std::net::IpAddr, u16),
    /// Struct-like variant: Named fields for complex payloads
    PacketReceived {
        interface: String,
        bytes_transferred: usize,
        checksum_valid: bool,
    },
    /// Wrapped error state
    FatalError(String),
}
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph DataModeling["Rust Algebraic Data Types (ADT)"]
        direction TB
        subgraph ProductType["Product Type: Struct"]
            direction TB
            ST1["struct ServerConfig<br/>All fields coexist in memory"]
            ST2["host: String (24 bytes on stack)<br/>port: u16 (2 bytes + 6B padding)<br/>tls: bool (1 byte + 7B padding)"]
            ST3["Total Memory Size =<br/>Sum of fields + padding (32 bytes)"]
            ST1 --> ST2 --> ST3
        end
        subgraph SumType["Sum Type: Enum"]
            direction TB
            EN1["enum NetworkEvent<br/>Exactly ONE variant is active"]
            EN2["LinkDown: Unit<br/>ConnectionEstablished: Tuple<br/>PacketReceived: Struct payload"]
            EN3["Total Memory Size =<br/>Tag Discriminant (1-8B) +<br/>Largest Variant Payload + padding"]
            EN1 --> EN2 --> EN3
        end
    end

Niche Filling and Null Pointer Optimization

A remarkable optimization in rustc is Null Pointer Optimization (NPO). For types that have an invalid bit pattern (such as references &T, Box<T>, or std::num::NonZeroU64 where pointer 0x0 is forbidden), the compiler uses 0x0 to represent the None variant of Option<T>.

Verifying Zero-Cost Option Optimization
use std::mem::size_of;

// Both occupy exactly 8 bytes on a 64-bit architecture!
assert_eq!(size_of::<&str>(), 16);
assert_eq!(size_of::<Option<&str>>(), 16); // Zero overhead! No extra tag byte!

4. Pattern Matching: Precision Control Flow

Rust provides the match expression, which enforces exhaustiveness at compile time: every possible branch must be handled, guaranteeing zero unhandled edge cases.

Exhaustive match, Guards, and Destructuring

Advanced Pattern Matching with Guards and Destructuring
fn process_event(event: &NetworkEvent) -> String {
    match event {
        NetworkEvent::LinkDown => "Physical network link severed.".to_string(),
        NetworkEvent::ConnectionEstablished(ip, port) if ip.is_loopback() => {
            format!("Local loopback connection on port {port}")
        }
        NetworkEvent::ConnectionEstablished(ip, port) => {
            format!("External peer connected: {ip}:{port}")
        }
        NetworkEvent::PacketReceived { bytes_transferred, checksum_valid: true, .. } => {
            format!("Processed {bytes_transferred} authenticated bytes.")
        }
        NetworkEvent::PacketReceived { checksum_valid: false, .. } => {
            "Corrupted frame detected; packet dropped.".to_string()
        }
        NetworkEvent::FatalError(err) => format!("Kernel panic alert: {err}"),
    }
}

Ergonomic Syntax: if let, while let, and let else

When you only care about a single variant, full match blocks can feel verbose. Rust provides three expressive alternatives:

Ergonomic Matching Constructs
let raw_config: Option<u16> = Some(8080);

// 1. if let: Execute only if matching
if let Some(port) = raw_config {
    println!("Binding daemon to port {port}");
}

// 2. let else (Rust 1.65+): Idiomatic guard clause with mandatory diverging break/return
fn parse_port_or_bail(opt: Option<u16>) -> u16 {
    let Some(port) = opt else {
        // The else block MUST diverge (return, break, continue, or panic!)
        println!("Port configuration absent; defaulting to 80");
        return 80;
    };
    port
}

5. Eliminating Null: The Option<T> Enum

British computer scientist Sir Tony Hoare famously referred to his invention of the null reference in 1965 as his "billion-dollar mistake". Rust prevents null pointer dereference bugs by entirely lacking a null keyword. Absent values are modeled explicitly via Option<T>:

pub enum Option<T> {
    None,
    Some(T),
}

Functional Combinators: Replacing Imperative Checks

Avoid calling .unwrap() in production systems. Instead, chain monadic functional combinators:

Idiomatic Option Combinators
let raw_timeout: Option<&str> = Some("4500");

// Transform Some(&str) -> Some(u64) -> add 500ms safety buffer
let calculated_timeout: u64 = raw_timeout
    .and_then(|s| s.parse::<u64>().ok()) // Parses string to Result, converts to Option
    .map(|ms| ms + 500)                  // Transforms inner value if Some
    .unwrap_or(3000);                    // Fallback default if None

assert_eq!(calculated_timeout, 5000);

6. Robust Error Architecture: Result<T, E> and ?

Rust divides runtime failures into two distinct categories:

  1. Unrecoverable Errors (panic!): Invariant violations, out-of-bounds array access, or fatal internal bugs. Triggers stack unwinding (or immediate abort) and cleans up allocated memory via RAII Drop.
  2. Recoverable Errors (Result<T, E>): Foreseeable operational issues (network timeouts, missing files, corrupted input, malformed protocol headers).
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph ErrorHandling["Rust Dual Error Architecture"]
        direction TB
        E1["Operation or Syscall Execution"]
        E1 --> C1{"Failure Occurred?"}

        C1 -- No --> S1["Ok(T) or Some(T)<br/>Normal Execution Pipeline"]

        C1 -- Yes --> C2{"Recoverable Error?"}

        C2 -- Yes --> R1["Result::Err(E)<br/>Explicit Error Value"]
        R1 --> R2["Propagate via ? operator<br/>Pattern match or handle locally"]
        R2 --> R3["Graceful Degradation or<br/>Structured Error Log"]

        C2 -- No --> U1["Unrecoverable Failure<br/>Bug, Out of Bounds, Invariant Violation"]
        U1 --> U2["panic! Triggered"]
        U2 --> U3["Stack Unwinding and RAII Drop<br/>or Abort without Memory Corruption"]
    end

The ? (Try) Operator

The ? operator unwraps Ok(T) if successful, or immediately returns Err(From::from(e)) from the enclosing function:

Error Propagation with the ? Operator
use std::fs::File;
use std::io::{self, Read};

fn read_kernel_hostname() -> Result<String, io::Error> {
    let mut file = File::open("/proc/sys/kernel/hostname")?; // Returns io::Error on failure
    let mut hostname = String::new();
    file.read_to_string(&mut hostname)?;                    // Returns io::Error on failure
    Ok(hostname.trim().to_string())
}

7. Designing Production Error Types

In production Rust applications and libraries, string errors (Err("failed")) are an anti-pattern. High-performance software requires structured, strongly-typed error enums.

Handcrafted Domain Error Enum (Zero Dependencies)
use std::fmt;
use std::error::Error;

#[derive(Debug, PartialEq, Eq)]
pub enum ServerConfigError {
    IoFailure(String),
    InvalidPort { requested: u32, max: u16 },
    MissingMandatoryKey(&'static str),
}

impl fmt::Display for ServerConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::IoFailure(msg) => write!(f, "I/O subsystem failure: {msg}"),
            Self::InvalidPort { requested, max } => {
                write!(f, "Port {requested} exceeds permissible maximum {max}")
            }
            Self::MissingMandatoryKey(key) => write!(f, "Required configuration key missing: {key}"),
        }
    }
}

impl Error for ServerConfigError {}

[!NOTE] In large-scale Cargo projects, the community standard crate thiserror automates the generation of Display and Error via procedural macros (#[derive(Error)]), while anyhow is favored in application binary code for effortless error aggregation with contextual traces.


8. Hands-On Systems Lab: web_log_parser.rs

We will construct a production-ready, zero-panic Apache/Nginx web server access log parser. It leverages classic named structs, rich sum type enums, custom domain errors, and exhaustive pattern matching.

web_log_parser.rs (Standalone Executable Systems Lab)
use std::fmt;
use std::error::Error;
use std::str::FromStr;

/// HTTP Request Methods (Sum Type)
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
    Patch,
    Head,
    Options,
}

impl FromStr for HttpMethod {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "GET" => Ok(HttpMethod::Get),
            "POST" => Ok(HttpMethod::Post),
            "PUT" => Ok(HttpMethod::Put),
            "DELETE" => Ok(HttpMethod::Delete),
            "PATCH" => Ok(HttpMethod::Patch),
            "HEAD" => Ok(HttpMethod::Head),
            "OPTIONS" => Ok(HttpMethod::Options),
            other => Err(ParseError::UnknownHttpMethod(other.to_string())),
        }
    }
}

/// HTTP Response Status Category
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum HttpStatusCategory {
    Success,      // 2xx
    Redirection,  // 3xx
    ClientError,  // 4xx
    ServerError,  // 5xx
    Unknown,
}

/// Structured Web Log Record (Product Type)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogRecord {
    pub client_ip: String,
    pub timestamp: String,
    pub method: HttpMethod,
    pub path: String,
    pub status_code: u16,
    pub response_bytes: u64,
}

impl LogRecord {
    /// Evaluates HTTP status category via pattern range matching
    pub fn status_category(&self) -> HttpStatusCategory {
        match self.status_code {
            200..=299 => HttpStatusCategory::Success,
            300..=399 => HttpStatusCategory::Redirection,
            400..=499 => HttpStatusCategory::ClientError,
            500..=599 => HttpStatusCategory::ServerError,
            _ => HttpStatusCategory::Unknown,
        }
    }
}

/// Strongly-Typed Domain Error
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError {
    EmptyLine,
    InsufficientFields { found: usize, expected: usize },
    MalformedRequestSection,
    UnknownHttpMethod(String),
    InvalidStatusCode(String),
    InvalidByteCount(String),
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyLine => write!(f, "Log line was empty or contained only whitespace"),
            Self::InsufficientFields { found, expected } => {
                write!(f, "Insufficient columns: found {found}, expected at least {expected}")
            }
            Self::MalformedRequestSection => write!(f, "Quoted HTTP request segment is malformed"),
            Self::UnknownHttpMethod(m) => write!(f, "Unrecognized HTTP verb: '{m}'"),
            Self::InvalidStatusCode(s) => write!(f, "Cannot parse integer HTTP status code: '{s}'"),
            Self::InvalidByteCount(b) => write!(f, "Cannot parse response payload byte size: '{b}'"),
        }
    }
}

impl Error for ParseError {}

/// Pure, Fallible Parser for Apache/Nginx Combined Log Lines
pub fn parse_log_line(line: &str) -> Result<LogRecord, ParseError> {
    let trimmed = line.trim();
    if trimmed.is_empty() {
        return Err(ParseError::EmptyLine);
    }

    // Split on double quotes to separate prefix, quoted request, and suffix
    let parts: Vec<&str> = trimmed.split('"').collect();
    if parts.len() < 3 {
        return Err(ParseError::MalformedRequestSection);
    }

    // 1. Parse client IP and timestamp from prefix
    let prefix_tokens: Vec<&str> = parts[0].split_whitespace().collect();
    if prefix_tokens.is_empty() {
        return Err(ParseError::InsufficientFields { found: 0, expected: 1 });
    }
    let client_ip = prefix_tokens[0].to_string();

    // Extract timestamp between [ and ]
    let timestamp = parts[0]
        .find('[')
        .and_then(|start| parts[0].find(']').map(|end| &parts[0][start + 1..end]))
        .unwrap_or("UNKNOWN_TIMESTAMP")
        .to_string();

    // 2. Parse request segment: "GET /api/v1/telemetry HTTP/1.1"
    let request_tokens: Vec<&str> = parts[1].split_whitespace().collect();
    if request_tokens.len() < 2 {
        return Err(ParseError::MalformedRequestSection);
    }
    let method = HttpMethod::from_str(request_tokens[0])?;
    let path = request_tokens[1].to_string();

    // 3. Parse status code and response size from suffix
    let suffix_tokens: Vec<&str> = parts[2].split_whitespace().collect();
    if suffix_tokens.len() < 2 {
        return Err(ParseError::InsufficientFields {
            found: suffix_tokens.len(),
            expected: 2,
        });
    }

    let status_code = suffix_tokens[0]
        .parse::<u16>()
        .map_err(|_| ParseError::InvalidStatusCode(suffix_tokens[0].to_string()))?;

    let response_bytes = match suffix_tokens[1] {
        "-" => 0, // HTTP 304 or empty response marker
        raw => raw
            .parse::<u64>()
            .map_err(|_| ParseError::InvalidByteCount(raw.to_string()))?,
    };

    Ok(LogRecord {
        client_ip,
        timestamp,
        method,
        path,
        status_code,
        response_bytes,
    })
}

fn main() {
    println!("=== LINUX NGINX ACCESS LOG PARSER (ZERO-PANIC ARCHITECTURE) ===\n");

    let raw_logs = [
        r#"192.168.1.105 - - [07/Nov/2026:12:30:45 +0000] "GET /api/v1/telemetry HTTP/1.1" 200 4826"#,
        r#"10.0.0.12 - - [07/Nov/2026:12:30:46 +0000] "POST /api/v1/auth HTTP/1.1" 401 128"#,
        r#"172.16.0.5 - - [07/Nov/2026:12:30:47 +0000] "DELETE /admin/config HTTP/1.1" 403 89"#,
        r#"192.168.1.200 - - [07/Nov/2026:12:30:48 +0000] "GET /static/bundle.js HTTP/1.1" 304 -"#,
        r#"10.0.0.99 - - [07/Nov/2026:12:30:49 +0000] "PROPFIND /webdav/docs HTTP/1.1" 405 210"#,
        r#"192.168.1.105 - - [07/Nov/2026:12:30:50 +0000] "GET /api/v2/metrics HTTP/1.1" 500 512"#,
        r#"CORRUPTED_LINE_WITHOUT_QUOTES_OR_VALID_COLUMNS"#,
    ];

    let mut total_requests = 0;
    let mut total_bytes = 0;
    let mut client_errors = 0;
    let mut server_errors = 0;
    let mut rejected_lines = 0;

    for (idx, line) in raw_logs.iter().enumerate() {
        match parse_log_line(line) {
            Ok(record) => {
                total_requests += 1;
                total_bytes += record.response_bytes;

                match record.status_category() {
                    HttpStatusCategory::ClientError => client_errors += 1,
                    HttpStatusCategory::ServerError => server_errors += 1,
                    _ => {}
                }

                println!(
                    "[{:>2}] OK  | {:<6?} | {:<3} ({:<11?}) | {:<20} | {:>5} B",
                    idx + 1,
                    record.method,
                    record.status_code,
                    record.status_category(),
                    record.path,
                    record.response_bytes
                );
            }
            Err(err) => {
                rejected_lines += 1;
                eprintln!("[{:>2}] ERR | Line rejected: {err}", idx + 1);
            }
        }
    }

    println!("\n--- TELEMETRY AGGREGATION SUMMARY ---");
    println!("Total Valid Requests: {total_requests}");
    println!("Aggregate Volume:     {total_bytes} bytes");
    println!("Client 4xx Errors:    {client_errors}");
    println!("Server 5xx Errors:    {server_errors}");
    println!("Corrupted Dropped:    {rejected_lines}");
}

Building & Running the Lab

Compile and execute the standalone lab directly with rustc:

Compiling and Executing the Standalone Systems Lab
rustc -O web_log_parser.rs
./web_log_parser

Expected Terminal Output:

=== LINUX NGINX ACCESS LOG PARSER (ZERO-PANIC ARCHITECTURE) ===

[ 1] OK  | Get    | 200 (Success    ) | /api/v1/telemetry    |  4826 B
[ 2] OK  | Post   | 401 (ClientError) | /api/v1/auth         |   128 B
[ 3] OK  | Delete | 403 (ClientError) | /admin/config        |    89 B
[ 4] OK  | Get    | 304 (Redirection) | /static/bundle.js    |     0 B
[ 5] ERR | Line rejected: Unrecognized HTTP verb: 'PROPFIND'
[ 6] OK  | Get    | 500 (ServerError) | /api/v2/metrics      |   512 B
[ 7] ERR | Line rejected: Quoted HTTP request segment is malformed

--- TELEMETRY AGGREGATION SUMMARY ---
Total Valid Requests: 5
Aggregate Volume:     5555 bytes
Client 4xx Errors:    2
Server 5xx Errors:    1
Corrupted Dropped:    2


Where to Go Next

With composite types, algebraic enums, and error handling established, proceed to dynamic memory, contiguous heap structures, and UTF-8 string mechanics:

  • Part 2.1: Collections, Strings & Slices


    Explore Vec<T>, string slices &str, heap reallocation overhead, BTreeMap vs HashMap, and zero-cost iterator pipelines.

    Advance to Part 2.1