Loading, Error, Empty: State as a First-Class Design Most teams design the happy path carefully — the populated list, the loaded form, the successful chart — and then bolt a spinner and an error string onto it afterward, as an implementation detail rather than a design decision. But loading, error, and empty are just as much states of the UI as the data itself, and they deserve the same rigor. This is the lesson that reunites the chapter, because how you model these states is a decision that has nothing to do with which framework you're in. The real shape of an async operation Any async UI operation has at minimum four distinct states: it hasn't started yet, it's in progress, it succeeded — with zero or more items, which are meaningfully different — or it failed. Most codebases don't model this as four states. They model it as two or three independent booleans: This looks harmless and it's the root of the whole problem. Two independent booleans can combine into four states — , , , — and one of those, both loading and erroring at once, should be impossible. Nothing in the type stops it from happening, and in a real codebase, it eventually will. The fix: one discriminated value, not several flags Model the async operation as a single value with a fixed, mutually-exclusive set of shapes — a discriminated union — instead of several independently-settable flags describing the same thing. In Angular In React The identical shape, via or — or, more often in real production code, a query library like TanStack Query that already models exactly this state machine for you, with , , , and that are mutually exclusive by construction: Either way, the render logic is a single exhaustive switch over one value — not a nest of guesses layered on top of independently-mutable flags. The framework changes the syntax. Never the decision. The Trade-off: Is a Discriminated State Worth the Extra Type? A pair of booleans is less code to declare than a discriminated union. The cost of the simpler version only shows up once a second state needs representing independently — which, for almost any real async UI, doesn't take long. The pain, concretely v1: A component has and . Two flags for what's really one status. Fine for now — there's no empty-state distinction yet, and the two flags happen to never conflict in practice. v2: Product asks for a distinct empty-results message, different from "no data yet." The fastest edit is a third flag, , set independently wherever the data turns out to have zero items. v3: A genuine bug ships: a fast request's error handler sets and , but a slow, now-stale request's block — from a previous , superseded call — fires afterward and resets back to . The UI briefly shows a spinner stacked directly on top of an error message, because nothing in the type system said those two states couldn't coexist. v4: A new engineer, unable to fully trust the existing flag combinations, adds a fourth boolean — — specifically to work around the confusion from v3, deepening the exact problem it was meant to fix. Side-by-side Independent booleans Discriminated union --- --- --- Can represent an impossible combination (loading and error at once) Yes — nothing prevents it No — the type only allows one status at a time Adding a new state (e.g., "stale, revalidating in background") Another independent flag, another combination to reason about One more member added to the union; existing cases are unaffected What to render, at a glance Requires reading several flags together to infer the real state Read one field; the current state is unambiguous Testing a specific state in isolation Must construct a flag combination and hope it's reachable in practice Construct one value; it's guaranteed to be a real, reachable state The signal to switch The moment a second independent boolean or nullable field is about to describe the status of the same async operation — that's the trigger to collapse them into one discriminated value. A single flag with nothing else to combine it with isn't a problem yet. Start simple A one-shot fetch with no error handling and no meaningful empty state — a static config file loaded once at startup, say — can stay as a single boolean; there's nothing yet to combine it with incorrectly. The discriminated-union discipline earns its cost the moment a second state — error, or a distinct empty case — needs representing independently of the first. The rule Model async UI as one discriminated state, not a pile of independent booleans. Loading, error, empty, and success are mutually exclusive facts about the same operation — represent them as one value with a fixed set of shapes, not as flags that can silently combine into states that were supposed to be impossible. That closes the chapter's real thesis: reactivity isn't about which library or primitive you reach for. It's about whether the shape of your state can even represent the bugs you're afraid of — or whether it quietly allows them, one convenient flag at a…