Tags: web-dev concept

State Machines

Date: 2026-08-17


Modelling a process as a fixed set of states and the legal transitions between them. It removes a whole class of bug by making impossible states impossible to represent, rather than merely unlikely.


A state machine defines every state a thing can be in, and every permitted move between them. Anything not listed cannot happen.

The problem it removes

The usual way state gets modelled — a handful of independent booleans:

isLoading   isError   hasData
   false     false     false     ← initial
   true      false     false     ← loading
   false     false     true      ← loaded
   false     true      false     ← failed

   true      true      true      ← ???
   true      true      false     ← ???

Three booleans is eight combinations, of which four are meaningless. Nothing prevents them, so every render has to defend against states that should not exist — and the bug where a spinner shows over an error message is exactly one of those combinations occurring.

The state machine version:

state: 'idle' | 'loading' | 'ready' | 'error'

Four states, all meaningful, illegal ones unrepresentable. The defensive checks disappear because the situation can’t arise — Type Systems.

Transitions are the other half

Listing states is only useful if you also constrain the moves:

STATE      EVENT       → NEXT STATE

idle       FETCH       → loading
loading    RESOLVE     → ready
loading    REJECT      → error
loading    CANCEL      → idle
ready      FETCH       → loading
error      RETRY       → loading

anything else: ignored

“Anything else: ignored” is where the value is. A RESOLVE arriving while in idle — a response from a request that was cancelled — is discarded rather than writing stale data into the UI. That’s a race condition eliminated by construction — Race Conditions.

A worked example

An order, which everyone models and most model badly:

                 ┌──────────┐
                 │ pending  │
                 └────┬─────┘
              pay ────┤──── cancel
                 ▼         ▼
            ┌────────┐  ┌───────────┐
            │  paid  │  │ cancelled │
            └───┬────┘  └───────────┘
        fulfil ─┤─ refund
                ▼       ▼
        ┌───────────┐ ┌──────────┐
        │ fulfilled │ │ refunded │
        └─────┬─────┘ └──────────┘
       return ┤
              ▼
        ┌──────────┐
        │ returned │
        └──────────┘

What the diagram makes obvious: you cannot fulfil a cancelled order, you cannot cancel a fulfilled one, and refunded is reachable from paid but not from pending. Those rules exist in every ecommerce system; the question is whether they’re written down or scattered across twenty if statements — Revenue Metrics.

Where they earn their place

  • Async data fetching — the four-state example above, in every component that loads anything
  • Checkout and multi-step forms — steps with rules about which are reachable
  • Order, subscription and payment lifecycles
  • Media players, uploads, wizards — anything with a play/pause/buffer/ended character
  • Feature rollout stages — off, internal, percentage, on — Feature Flags
  • Parsers and tokenisers, where the theory came from

Where they’re overkill

Two states with one transition is a boolean. Reaching for a state machine there adds ceremony and removes nothing, and the vault’s compression rules apply to code as much as to notes.

The threshold is roughly: three or more states, or any transition that must be forbidden. Below that, a boolean is honest.

Practical notes

  • Model the state, derive the display. isLoading becomes state === 'loading' — computed, never stored, so it can’t disagree
  • Attach data to the state, not beside it. A ready state carries the data; an error state carries the error. Neither carries the other, which is what stops stale data surviving a failed refetch
  • Name states after what is true, not what is happening — ready rather than finishedLoading
  • Draw it before coding it. Most of the value arrives during the drawing, when someone asks whether a transition that nobody had considered should exist
  • The transition table is testable, exhaustively, in a way scattered conditionals aren’t