All articles
Software Development5 min read

34. Prepare Your Code For The Unexpected

Defensive programming is not paranoia, it is craftsmanship. Why your code needs guardrails, strict types and layered checks for the day production finds the path you forgot.

Delft blue tile reading "Prepare Your Code For The Unexpected" — Developers Tiles of Wisdom

These are always difficult situations for a developer. A bug is found in production. A user clicks a button, but nothing happens. No error, no message, just nothing.

Of course, everything works fine on staging, test and development. Just not in production. For one specific user. And of course, that user also happens to be the client. How is that possible?

After a lot of searching, analyzing and debugging, the cause is found. This user still has an old value somewhere in local storage. Or in a cookie. Or in some piece of data that was stored at some point and never cleaned up. A value that, according to the current application, should no longer be able to exist, but for this user it still does.

The code expects value A, B or C, but this user still has value D. Somewhere in the code there is a switch that only takes A, B and C into account. No default. No fallback. No error. No logging. So nothing happens.

🛡️ And that is exactly where defensive programming starts.

Not with the question: "Which values do I expect?" But with the question: "What happens if something else comes in anyway?"

Because your code does not live in the perfect world that exists in your head while you are writing it. Your code lives in production. And production is messy.

Data changes. APIs change. Requirements change. Other developers change your code. You change your own code three months later. A migration goes slightly differently than expected. A feature flag stays enabled for too long. An external dependency behaves differently. A value that "can never happen" suddenly happens anyway.

That is why I believe defensive programming is not paranoia. It is craftsmanship.

A switch should have a default. Even when TypeScript says all values are covered. Even when your application currently only uses the values that are already in your switch. Even when the backend promises that nothing else will ever be returned.

Because "right now" is not the same as "always". And "should not happen" is not the same as "cannot happen".

That does not mean you should write scared code. It means you should write code that fails clearly, safely and predictably.

The goal is not to silently accept broken states. The goal is to make impossible states visible as early as possible.

🔎 That is also why I want to use TypeScript as strictly as possible. No loose types. No any, unless there is a very good reason for it. No assumptions about data when you are not actually sure they are correct. No trusting external input without validation.

Types are one safety layer. Tests are a second. ESLint is a third. Runtime validation is a fourth. CI checks are a fifth. Pre-commit checks are a sixth. Pre-push checks are a seventh. And pull request checks are an eighth.

No single layer is perfect. But together they form a safety net.

🚀 And recently, I was reminded again how valuable such a safety net is.

In a recent project, I spent a lot of time strengthening the quality foundation of a frontend application. That meant adding tests, making TypeScript types stricter, making code paths more explicit, running checks before code is committed, running checks before code is pushed, running checks before a pull request can be merged, and extending and tightening the CI/CD pipeline.

That is not always the most visible work. And honestly, sometimes it feels slow. Because every check takes time. Every strict type definition requires attention. Every test needs to be written, reviewed and maintained.

But then came the moment where it mattered.

We had to do a large release. Not a small bugfix. Not one isolated change. But a large release with many new features and code changes throughout the entire application.

Of course, we had tested everything extensively. But there is always a tense moment right before production. Because no matter how well you test, you can never fully simulate how real users behave in a real production environment.

So we deployed. And it went smoothly. Completely smoothly.

No bugs reported by users. No unexpected production issues. No panic. The release simply worked.

That does not happen by accident. It happens because the codebase has guardrails. Because mistakes are found early. Because wrong assumptions become visible before users hit them. Because the system is designed to better handle unexpected input, unexpected changes and future requirements.

That, to me, is the real value of defensive programming.

It is not about writing more code. It is about allowing fewer things to fail silently. It is about making future changes safer. It is about protecting your users. Protecting your team. And especially: protecting your future self.

So yes: add that default. Use strict types. Validate your assumptions. Write the test. Let the checks run. Make invalid states impossible where you can. And where you cannot: make sure they fail loudly and safely.

Because production will eventually find that one path you did not think about.

Good code is not only code that works when everything goes right. Good code is code that is prepared for when something goes wrong.

💬 Need help making your frontend application more robust, safer and easier to maintain? For example with better tests, stricter TypeScript types, smarter CI/CD checks or a thorough quality analysis of your codebase?

Feel free to contact me. I am happy to help turn a fragile codebase into a codebase you can confidently keep building on.

Related articles