Avoiding a major source of bugs

Avoiding a major source of bugs


A common cause of bugs is when someone changes a piece of code without understanding everywhere that it is used, and breaks an assumption that dependent code elsewhere was making.


This can manifest in many ways. In dynamic languages it could be as simple as changing the function signature. In non-functional languages it could be changing a side effect that is relied on. The more places a piece of code is used, the more likely it is that every observable property of it is relied upon by somebody, making it more dangerous to change.


The common solution I see to this issue is writing ever more automated tests that run before merging to main. It's better than nothing, but it's a heavy handed approach and can give you false confidence. Tests have many limitations. They usually only cover a tiny fraction of the possible states your program can be in, or if they are too rigid, they can make code difficult to change without refactoring hundreds of tests. I love automated tests more than most, but they are the last line of defense.


The first line of defense is understanding the codebase. Someone who understands the codebase in it's entirety will avoid classes of bugs that a new person would stumble into. A well designed codebase shouldn't need someone to understand the whole thing to make changes though.


Another strategy is to avoid reusing code too much. Even when most of the code is duplicated, if two functions are expected to evolve in different directions, it is better to leave the code duplicated. You have to decide, are you deduplicating code because it could cause a bug if they get out of sync? Or are you deduplicating to save typing. If it's the latter then think twice. When you reuse code, the next person who comes along to change it, including you, is now expected to understand both use cases for that code.


If you need to reuse code, try to do it with a pure function. Pure functions reduce the observable parts of your code that people may rely on. Less dependencies means less broken assumptions.


Use the type system. Type systems are basically a really powerful automated test built into your language. They make pure functions even more powerful because now the function signature can be relied on. Learn how to best use the type system to encode the assumptions you make.


Some honorable mentions: write less code. Less code means less code to understand. Design features that are less complex. Simpler features are easier to understand. Reduce the API surface, including what you accept and what you return. Avoid deeply nested call graphs. Flatter code is easier to read and understand the impact of changes.


With these strategies in mind, hopefully you can avoid a few more bugs reaching production.