The idea
What it is
State is a behavioral pattern with a one-sentence intent: let an object change its behavior when its internal state changes, by delegating to a state object instead of switching on a flag. Think of a music player. It has one pressPlay() button, but that button does three completely different things: when the player is stopped, it starts playback from the top; when it's playing, it pauses; when it's paused, it resumes. The behavior of the very same method depends entirely on what mode the object is in right now.
The naive way to code this is a state flag — this.state = "PLAYING" — and a switch (or if/else chain) on that flag inside pressPlay(). Then another switch inside pressStop(). And another inside pressNext(). Every method grows its own copy of the same branching, and adding one new state (say, Buffering) means hunting down and editing every one of those switches. The State pattern flips this inside out: each state becomes its own class — StoppedState, PlayingState, PausedState — all implementing the same interface. The player keeps a reference to its current state object and forwards every call to it. The mode-specific logic lives in exactly one place per mode.
The one sentence to remember
When an object's behavior depends on a mode that changes at runtime, don't switch on a flag in every method — give each mode its own class and let the object delegate to its current state, which also decides what the next state is.
Same word, two lessons
This lesson is the behavioral design pattern called State — states as classes inside your code. The [[state-diagrams]] lesson is the UML notation for drawing states and transitions on paper. They fit together beautifully: a state diagram is often the blueprint, and the State pattern is how you implement that blueprint so each circle in the drawing becomes one class.
Mechanics
How it works
The three participants
A textbook State setup has three moving parts, and once you see them in the player example you'll recognize them everywhere:
- Context — the object the outside world talks to. Here it's the
Player. It holds one field,current: State, and its public methods (pressPlay(),pressStop()) do nothing but forward the call tocurrent. The context contains zero mode-specific logic. - State interface — one small interface with one method per action the context supports:
pressPlay(player),pressStop(player). Every concrete state must answer all of them, so no mode can 'forget' to handle an action. - Concrete states — one class per mode:
StoppedState,PlayingState,PausedState. Each implements the actions the way that mode behaves, and — crucially — each decides the next state by callingplayer.setState(new PlayingState()).
interface State {
pressPlay(player: Player): void;
pressStop(player: Player): void;
}
class Player {
current: State = new StoppedState(); // start stopped
setState(s: State) { this.current = s; }
// the context only delegates — no switch, no flags
pressPlay() { this.current.pressPlay(this); }
pressStop() { this.current.pressStop(this); }
}
class StoppedState implements State {
pressPlay(p: Player) {
console.log("▶ start from 0:00");
p.setState(new PlayingState()); // the STATE picks the next state
}
pressStop() { console.log("already stopped"); }
}
class PlayingState implements State {
pressPlay(p: Player) {
console.log("⏸ pause");
p.setState(new PausedState());
}
pressStop(p: Player) {
console.log("⏹ stop");
p.setState(new StoppedState());
}
}Follow one call through: you press play while music is playing. player.pressPlay() runs this.current.pressPlay(this). Because current happens to be a PlayingState, that class's version runs — it pauses playback and swaps the player over to a fresh PausedState. The next pressPlay() lands on PausedState and resumes. The player object never asked "what state am I in?" — polymorphism answered the question for it.
Transitions live inside the states
This is the detail people miss on first read: it's not just that behavior is split into classes — the transition logic moves too. StoppedState.pressPlay() ends with player.setState(new PlayingState()). Each state knows which states can legally follow it, so the whole transition map is spelled out in the state classes themselves, not scattered across the context. Illegal moves become trivially easy to express: StoppedState.pressStop() simply does nothing (or logs "already stopped") — there's no way to accidentally fall through to the wrong branch of a shared switch.
The smell this pattern cures
Here's the code State replaces — and why it rots. Notice it's the same three-way branch, duplicated once per method:
class Player {
state = "STOPPED"; // a string flag
pressPlay() {
switch (this.state) { // branch #1
case "STOPPED": /* start */ this.state = "PLAYING"; break;
case "PLAYING": /* pause */ this.state = "PAUSED"; break;
case "PAUSED": /* resume */ this.state = "PLAYING"; break;
}
}
pressStop() {
switch (this.state) { /* branch #2 — same three cases again */ }
}
pressNext() {
switch (this.state) { /* branch #3 — and again... */ }
}
}- Adding a state = editing every method. Introduce
"BUFFERING"and you must find and extend every switch — miss one and you have a silent bug that only shows up in that mode. - With the pattern, adding a state = adding one class.
BufferingStateimplements the interface, existing classes barely change, and the compiler tells you exactly which methods the new mode must handle. That's [[open-closed]] in action: open to new states, closed to modification of the old ones. - The branches drift apart. Three copies of the same switch never stay in sync — someone fixes the PAUSED case in
pressPlay()but forgets it inpressStop().
State vs Strategy — same skeleton, different soul
Structurally, State looks identical to [[strategy]]: a context holding a reference to an interface, delegating calls, swapping the concrete object to change behavior. The difference is who swaps it and when. With Strategy, the client picks one algorithm up front ("sort with quicksort") and it rarely changes afterwards — the strategies don't know each other exist. With State, the object transitions itself: PlayingState knows about PausedState and actively installs it. States form a connected map of who-follows-whom; strategies are interchangeable strangers. If your objects hop between behaviors on their own as events happen, it's State. If a caller chooses one behavior and sticks with it, it's Strategy.
Where you'll meet it in the wild
- Order lifecycle —
Placed → Paid → Shipped → Delivered, wherecancel()behaves differently (or is forbidden) in each stage. - TCP connection — the classic GoF example:
Established,Listening,Closed, each handlingopen()/close()/send()in its own way. - Vending machine —
NoCoin,HasCoin,Dispensing: pressing the button with no coin does nothing; with a coin it vends. - Document workflow —
Draft → InReview → Published, wherepublish()is only meaningful from certain states andedit()may bounce a document back to Draft. - Game character modes —
Standing,Jumping,Ducking: the same controller button kicks, dives, or slides depending on the character's current mode.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
A music player with one ⏯ pressPlay() button. Press it while STOPPED and playback starts; press the exact same button while PLAYING and it pauses; press it while PAUSED and it resumes. Watch the glow: the player never decides anything itself — it hands every call to the current state card below it, and that card both performs the behavior and picks the next state (the glow physically moves). Flip the 🔀 switch-view chip to see what the same logic looks like as a naive switch(state) block — one red card where every new state means editing every method. Same button, three behaviors: that is the State pattern in one glance.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
One button, three behaviors
Open the prototype and look at the glow on the StoppedState card — that's the player's current state object. Now click ⏯ pressPlay() three times slowly. First click: StoppedState handles it, playback starts, and the glow jumps to PlayingState (watch the progress bar start moving). Second click: the same button now pauses, because PlayingState is answering the call. Third click: PausedState resumes. The button's label never changed — the object behind it did.
Transitions belong to the states
Click ⏹ pressStop() while PLAYING, then click it again while already STOPPED. The first press stops playback and moves the glow to StoppedState; the second is politely ignored — StoppedState decides that stopping while stopped does nothing, and no other class had to be consulted. Read the explain line each time: it always names which state class handled the call and which state it handed over to. That's the transition logic living inside the states.
See the code you escaped
Click the 🔀 switch-view chip. The three state cards collapse into a single red switch(state) block — the naive version — with the branch that would run highlighted for the player's current state. Press ⏯ pressPlay() a few times in this view and watch the highlight jump between cases of one big conditional. Note the caption: adding one state means editing this switch in every method. Flip back and appreciate that in the pattern view, a new state is just one new card.
In practice
When to use it — and what trips people up
Reach for State when behavior follows a mode
- Behavior visibly depends on a mode that changes at runtime. The same method call must do genuinely different things depending on the object's current condition — play/pause/resume, open/close a connection, cancel an order.
- Fat conditionals on a state flag in several methods. If you see the same
switch (this.state)(or if/else chain) copy-pasted across three or more methods, the State pattern is the standard refactoring — each case column becomes one class. - The object has an explicit lifecycle. Orders, documents, connections, and jobs all march through named stages with rules about which moves are legal. Encoding each stage as a class makes the legal moves explicit and the illegal ones impossible to reach.
- You already drew it. If the design phase produced a [[state-diagrams]] chart, the State pattern is the most direct translation: one class per circle, one
setStatecall per arrow.
When NOT to use it
- Two trivial states — a boolean is fine. A checkbox that's checked or not, a modal that's open or closed:
if (this.isOpen)in one or two places is clearer than four classes. State earns its keep at roughly three-plus states or three-plus methods that branch on the mode. - The branching lives in exactly one method. A single
switchin a single function isn't a smell — it's just a switch. The pattern pays off when the same branching is duplicated across many methods. - States never transition at runtime. If the 'mode' is picked once at construction and never changes, what you want is probably [[strategy]] (or plain configuration), not State.
What it gives you
- Adding a new state means adding one new class — existing states and the context stay untouched, instead of editing every switch in every method ([[open-closed]]).
- All behavior for one mode lives in one file — to understand what the player does while paused, you read PausedState and nothing else.
- Transitions are explicit and enforced — each state names exactly which states can follow it, so illegal moves (shipping a cancelled order) have nowhere to hide.
- The context shrinks to almost nothing — Player becomes a thin delegator with no branching, which makes it trivially easy to read and test.
Common mistakes
- Class count explodes for small problems — three modes and two actions already means one interface plus three classes where a couple of ifs once lived.
- The transition map gets scattered — with many states, 'who hands over to whom' is spread across every state class, and no single file shows the whole picture (keep the state diagram nearby).
- States often need access to the context's internals — passing the player into every call, or letting states poke its fields, can force you to widen its public surface.
- Object churn if you
newa state per transition — usually negligible, but hot paths often share stateless singleton instances of each state instead.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
// One method per action the player supports.
interface State {
pressPlay(player: Player): void;
pressStop(player: Player): void;
name(): string;
}
// CONTEXT — holds the current state and only delegates.
class Player {
private current: State = new StoppedState(); // start stopped
setState(s: State): void {
console.log(` → hand over to ${s.name()}`);
this.current = s;
}
pressPlay(): void { this.current.pressPlay(this); } // no switch!
pressStop(): void { this.current.pressStop(this); }
}
// CONCRETE STATES — behavior AND transitions live here.
class StoppedState implements State {
name() { return "StoppedState"; }
pressPlay(p: Player) {
console.log("▶ start playback from 0:00");
p.setState(new PlayingState()); // state picks the next state
}
pressStop(_p: Player) {
console.log("⏹ already stopped — nothing to do");
}
}
class PlayingState implements State {
name() { return "PlayingState"; }
pressPlay(p: Player) {
console.log("⏸ pause playback");
p.setState(new PausedState());
}
pressStop(p: Player) {
console.log("⏹ stop playback");
p.setState(new StoppedState());
}
}
class PausedState implements State {
name() { return "PausedState"; }
pressPlay(p: Player) {
console.log("▶ resume playback");
p.setState(new PlayingState());
}
pressStop(p: Player) {
console.log("⏹ stop playback");
p.setState(new StoppedState());
}
}
const player = new Player();
player.pressPlay(); // ▶ start (StoppedState answered)
player.pressPlay(); // ⏸ pause (PlayingState answered)
player.pressPlay(); // ▶ resume (PausedState answered)
player.pressStop(); // ⏹ stop (PlayingState answered)References & further reading
6 sources- Articlerefactoring.guru
State — Refactoring.Guru
The clearest illustrated walkthrough: the media-player-style problem, the Context/State/ConcreteState structure, and a careful section on how State differs from Strategy. Best first read.
- Docsen.wikipedia.org
State pattern — Wikipedia
Concise reference with UML structure and multi-language examples, including the classic Java media-player-style implementation with transitions inside the states.
- Booken.wikipedia.org
Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides
The original 'Gang of Four' book. Its State chapter uses the famous TCPConnection example — Established, Listening, Closed as classes — the primary source for the pattern's intent.
- Bookgameprogrammingpatterns.com
State — Game Programming Patterns (Robert Nystrom)
A free, wonderfully written chapter that grows the pattern from raw if-chains to finite state machines, using a game character (standing/jumping/ducking) as the running example.
- Articlerefactoring.guru
Replace Type Code with State/Strategy — Refactoring.Guru
The step-by-step refactoring recipe for turning an existing switch-on-a-flag class into the State pattern — exactly the migration you'd perform on the naive player.
- Docsen.wikipedia.org
Finite-state machine — Wikipedia
The theory underneath: states, events, and transitions as a formal model. Useful background for recognizing when a lifecycle is really a state machine wanting to be born.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Sign in to save your best score.
question 01 / 05
What is the core intent of the State pattern?
question 02 / 05
In the music-player example, where does the decision 'after playing, the next state is paused' live?
question 03 / 05
Your player uses switch (this.state) inside pressPlay(), pressStop(), and pressNext(). What happens when you add a new BUFFERING state — before and after applying the State pattern?
question 04 / 05
State and Strategy have nearly identical structure — a context delegating to an interface. What is the key difference?
question 05 / 05
Which situation is a poor fit for the State pattern?
0/5 answered