Symptom

An object's behaviour depends on its current state, and every method switches on that state to decide what to do. Adding a new state means editing every method in lockstep; one missed method silently makes the new state behave like an existing one in a context the test suite did not exercise.

Goal

Each state is a class implementing the operation interface; the host object delegates every operation to its current state object. Transitions happen by replacing the state object. Adding a new state is a new file; the host stays unchanged.

Before the pattern

class Order {
constructor() {
this.status = 'pending';
this.items = [];
}
confirm() {
if (this.status !== 'pending') throw new Error('can only confirm pending');
this.status = 'confirmed';
}
ship() {
if (this.status !== 'confirmed') throw new Error('can only ship confirmed');
this.status = 'shipped';
}
deliver() {
if (this.status !== 'shipped') throw new Error('can only deliver shipped');
this.status = 'delivered';
}
cancel() {
if (this.status === 'delivered') throw new Error('cannot cancel delivered');
if (this.status === 'shipped') refundAfterReturn();
this.status = 'cancelled';
}
}
// Every method switches on this.status. Adding 'On Hold' touches every method.

After the pattern

class PendingState {
confirm(order) { order.setState(new ConfirmedState()); }
ship() { throw new Error('cannot ship pending order'); }
deliver() { throw new Error('cannot deliver pending order'); }
cancel(order) { order.setState(new CancelledState()); }
}
class ConfirmedState {
confirm() { throw new Error('already confirmed'); }
ship(order) { order.setState(new ShippedState()); }
deliver() { throw new Error('cannot deliver before ship'); }
cancel(order) { order.setState(new CancelledState()); }
}
class ShippedState {
confirm() { throw new Error('already shipped'); }
ship() { throw new Error('already shipped'); }
deliver(order) { order.setState(new DeliveredState()); }
cancel(order) { refundAfterReturn(); order.setState(new CancelledState()); }
}
class DeliveredState {
confirm() { throw new Error('already delivered'); }
ship() { throw new Error('already delivered'); }
deliver() { throw new Error('already delivered'); }
cancel() { throw new Error('cannot cancel delivered'); }
}
class Order {
constructor() {
this.state = new PendingState();
this.items = [];
}
setState(s) { this.state = s; }
confirm() { this.state.confirm(this); }
ship() { this.state.ship(this); }
deliver() { this.state.deliver(this); }
cancel() { this.state.cancel(this); }
}
Example source: Illustrative example written for this site in the spirit of Design Patterns (Gamma, Helm, Johnson, Vlissides, Addison-Wesley, 1994), chapter 5. The book's running example is a TCP connection's state machine; this JavaScript adaptation uses an e-commerce order lifecycle because the state set is small enough to read in code and the per-transition rules are concrete.
Pressure

State-as-string with Repeated Switches in every method is the textbook smell pointing to State pattern. Every state-altering condition lives in N methods; adding one state is an N-method edit; the host class accumulates state-management logic that overshadows its actual responsibility.

Tradeoff

Each state lives in its own file; the class count grows with state count; the agent or human reading 'what does cancel mean in this domain' must navigate four files instead of one method. Transitions also require careful construction-cycle reasoning — new states must not capture stale references to the host.

Relief

Each state class is small, single-purpose, exhaustive in its operation set. Adding a state is one file + one initial-transition fix; the host's method definitions read as one-line delegations; tests for each state cover the operation set in isolation.

Trap

States that share significant behaviour across transitions (every state's cancel logs an audit event) reintroduce duplication across state files. Hoisting shared behaviour to a State superclass or composing in a common policy is essential; otherwise the pattern shifts the duplication problem from one file to many.