03

Make illegal states unrepresentable

#
  • pattern
  • established
  • fragment

Versions. Java example: records are final since Java 16, sealed since 17, record patterns and switch exhaustiveness checking since 21. Hence the “Java 21” label on the pane.

References: JEP 395: Records — JDK 16, JEP 409: Sealed Classes — JDK 17, JEP 440: Record Patterns — JDK 21, JEP 441: Pattern Matching for switch — JDK 21

Sum types instead of flag combinations. {isLoading, data, error} has 4 impossible states out of 8; a sum type has zero.

Why eight: three independent fields, each either present or absent — 2³. Four of them make sense: nothing started, loading, data arrived, error arrived. The other four — “loading and data at once”, “data and error”, “loading and error”, “everything at once” — are representable in the type, unreachable in meaning, and every code branch has to decide what to do with them. Usually it doesn’t, and the impossible state reaches the user. A sum type removes the question itself: there are exactly four states, because a fifth cannot be expressed.

Code example (fragment)
fragment
Java 21
// sealed + record + pattern matching
sealed interface FetchState<T> permits Loading, Loaded, Failed {}
record Loading<T>() implements FetchState<T> {}
record Loaded<T>(T data) implements FetchState<T> {}
record Failed<T>(Exception cause) implements FetchState<T> {}

String render(FetchState<String> s) {
    return switch (s) {                     // compiler demands exhaustiveness
        case Loading<String> l -> "spinner";
        case Loaded<String>(var data) -> data;
        case Failed<String>(var e) -> "error: " + e.getMessage();
    };
}
Rust
// enum is native; exhaustiveness comes for free
enum FetchState<T> {
    Loading,
    Loaded(T),
    Failed(anyhow::Error),
}

// match without a wildcard: add a variant and it won't compile until handled
Go
// no sum types; the closest substitute is a sealed interface + type switch.
// Honest assessment: exhaustiveness is checked by a linter, not the compiler
// (go-check-sumtype / exhaustive)
type fetchState interface{ isFetchState() }

type Loading struct{}
type Loaded struct{ Data []byte }
type Failed struct{ Err error }

func (Loading) isFetchState() {}
func (Loaded) isFetchState()  {}
func (Failed) isFetchState()  {}
Python
# union + match; mypy/pyright check exhaustiveness via assert_never
from dataclasses import dataclass
from typing import assert_never

@dataclass(frozen=True)
class Loading: pass

@dataclass(frozen=True)
class Loaded: data: bytes

@dataclass(frozen=True)
class Failed: cause: Exception

type FetchState = Loading | Loaded | Failed

def render(s: FetchState) -> str:
    match s:
        case Loading():      return "spinner"
        case Loaded(data):   return data.decode()
        case Failed(cause):  return f"error: {cause}"
        case _:              assert_never(s)   # static exhaustiveness

Last technical review: 2026-08-04 · Reference · 112 principles