Course contentsShow
← Rust
Lesson 910 of 1,9107. Error Handling PatternsFree lesson

Type-State Pattern for Error Elimination

Use the type system to make impossible error states unrepresentable at compile-time.

In this lesson, you will learn how to leverage Rust's type system to shift runtime error checks into compile-time guarantees, effectively making invalid states impossible to represent.

The Problem with Runtime Validation

Often, we define structures that allow "invalid" combinations of data. Consider a NetworkRequest object. If we use an Option<Response> field, we risk calling .unwrap() or handling a None variant when we haven't actually received the data yet. This leads to defensive coding and runtime panics.

Moving Errors to the Type System

Instead of checking if a request has a response at runtime, we can encode the state of the request into its type. By using generic type parameters, we define transitions that only exist if the previous state was valid.

struct Unsent;
struct Received(String);

struct Request<State> {
    state: State,
}

impl Request<Unsent> {
    fn new() -> Self { Self { state: Unsent } }
    
    // This method consumes the request and changes its type
    fn send(self) -> Request<Received> {
        Request { state: Received("Payload".to_string()) }
    }
}

impl Request<Received> {
    fn get_data(&self) -> &str {
        &self.state.0
    }
}

Why This Eliminates Errors

In the code above, the compiler tracks the state:

  1. You cannot call get_data() on a Request<Unsent> because that method simply doesn't exist on that type.
  2. You cannot accidentally "receive" a response before sending the request, because the send() function only accepts the Unsent state.

By partitioning logic into specific types, you eliminate "logic errors" (like calling an endpoint before authentication or reading a file before opening it) at the architectural level. You no longer need to return a Result or check an is_ready boolean because the code literally will not compile if you attempt to perform an operation in an invalid state.

This pattern shifts your error-handling focus: instead of writing code to handle what happens when things go wrong, you write types that ensure things can only go right.

Key Takeaway: The Type-State pattern uses generic markers to restrict method availability, turning potential runtime failures into compile-time logic errors.