We have all been there. It is Friday afternoon, deployment is in an hour, and you get a frantic slack message: "Production is down. Logs say 'InvalidOperationException: Cannot write to a closed stream'."
You sigh, open the code, and see it. Some developer—maybe a junior, maybe you three years ago before you discovered coffee—tried to call .write() on a file handler that hadn't been .open()-ed yet. Or worse, it was already .close()-ed.
The code relied on the developer remembering the order of operations. It relied on hope. And as I have told stakeholders a thousand times: hope is not a strategy.
If your code allows a developer to call a method when the object is in the wrong state, your code is the problem. Enter the Typestate Pattern.
The Problem: Boolean Flags and Prayers
Most developers handle state like this. They create a "god class" that does everything and uses internal flags to track what is happening.
class Rocket {
private isFueled: boolean = false;
private isReady: boolean = false;
fuel() {
this.isFueled = true;
}
arm() {
if (!this.isFueled) throw new Error("No fuel!");
this.isReady = true;
}
launch() {
if (!this.isReady) throw new Error("Not armed!");
console.log("Boom!");
}
}
// The usage relies on memory
const r = new Rocket();
// r.launch(); // Runtime Error!
r.fuel();
r.arm();
r.launch();
See the issue? The launch() method exists on the Rocket object even when the rocket isn't ready. The compiler is perfectly happy to let you write r.launch() immediately. It only blows up when the code actually runs—usually when a customer is watching.
The Hard Way: The Compiler as the Bouncer
The Typestate pattern moves state into the type system itself. Instead of one Rocket class that changes its internal flags, we have different classes representing each state. The launch() method simply does not exist until the rocket is ready.
If you try to launch an unfueled rocket, the code won't just fail; it won't compile.
// 1. Define the infrastructure
class UnfueledRocket {
fill(): FueledRocket { return new FueledRocket(); }
}
class FueledRocket {
arm(): ArmedRocket { return new ArmedRocket(); }
}
class ArmedRocket {
launch() { console.log("Boom!"); }
}
// 2. The Implementation
let state: any = new UnfueledRocket();
// 3. The Usage
if (state instanceof ArmedRocket) {
state.launch();
}
The Smart Way: Let the Platform Handle It
I know what you are thinking. "HH, that looks like a lot of code. I hate typing."
First, get a better keyboard. Second, look at the reality. The Typestate pattern eliminates entire categories of bugs, but it requires discipline and architecture.
If you don't want to, or can not do this kind of stuff, please use a dev platform like Servoy.
In Servoy, you don't build the infrastructure; you just define the rule. The platform enforces the state for you. We use a Calculation (a dynamic property) and bind it directly to the UI elements.
// Define the Logic (Calculation: 'isLaunchReady')
function isLaunchReady() {
// Evaluates automatically whenever 'fuel' or 'status' changes
return fuel_level > 0 && status == 'ARMED';
}
// The Usage
function onActionLaunch(event) {
// No 'if' checks needed.
// This function is PHYSICALLY UNREACHABLE if isLaunchReady is false.
plugins.dialogs.showInfoDialog("Success", "Boom!");
}
Do you see the difference? In the Form Designer, you simply bind elements.btnLaunch.enabled to the isLaunchReady calculation.
In the raw code example, I had to create three classes just to prove the rocket was ready. In Servoy, I wrote one line of logic. The engine watches the data. If fuel_level drops, the engine instantly recalculates isLaunchReady to false, and the "Launch" button turns gray.
You can't click it. You can't trigger the bug. The illegal state is unrepresentable, not because you wrote complex classes, but because the platform controls the reality of the application.
Stop Trusting Yourself
We like to think we are smart. We aren't. We are tired, distracted, and rushing to meet arbitrary deadlines set by people who think "agile" means "do it faster."
The Typestate pattern is a guardrail. It prevents you from doing the wrong thing. It shifts the burden of correctness from your fragile human memory to the cold, unyielding logic of the compiler (or the platform).
So next time you are designing a flow, make a choice: Architect it properly so illegal states are unrepresentable, or use a tool that solves it for you. Just stop using booleans and hope.
What is the worst "state-based" bug you have ever pushed to production? Let me know in the comments.
Top comments (0)