Phil Vuollet uses software to automate process to improve efficiency and repeatability. He writes about topics relevant to technology and business, occasionally gives talks on the same topics, and is a family man who enjoys playing soccer and board games with his children.
Perspective is everything. We developers tend to make a lot of assumptions that lead to mistakes in our code. These assumptions are rooted in our limited perspectives. We’re only human, after all.
Talking about these mistakes in the open helps us to avoid them in the future. Making mistakes is largely a good thing. It means we’re trying new things. They’re an inevitable part of the learning and growth process. It’s important to be able to make mistakes, take constructive criticism from others, be critical of our own work, and grow as we learn from them.
While it’s generally OK to make mistakes, you may not always be aware that you’re doing so. And that’s not a good thing. So to help you become more aware, here are 10 mistakes you’re probably baking into your source code—and you may not even know it. A handful of engineers, including myself, who work in various technologies, industries, and countries put together this list of the most common mistakes we’ve seen. Some we’ve made in our own source code at some point.
If you’re baking these into your own source code, it may be that you’re only using a limited set of ingredients. It’s time to expand your horizons and learn some new recipes. You’ll inevitably burn a few dishes, but that’s OK. You can’t make the perfect beef wellington on the first try. It’s all part of the learning process!
Mistake 1: No Configurable Logging Level
Basically, you should have different levels of logging. Some common logging levels include Debug, Error, Warning, Info, and Verbose.
The Naive Approach: Log everything as an error—or (the variant) hard-code the logging level. The trouble is you’ll end up logging errors that aren’t really errors. On the flip side, you might not log enough information to properly debug in production.
The Right Approach: Log at different levels. Set the logging level based on need. You don’t always need Debug-level logging, but sometimes you might. When you do, you should be able to change to it without changing any code or redeploying the application.
Mistake 2: Pointless/Incomplete Logging Messages
This mistake has the most impact when it comes to logging. Skip logging altogether and you could miss some impactful errors. Logging pointless information just adds noise. And when important information is missing from the message, it can really cause some problems with issue resolution.
The Naive Approach: Create log messages ad hoc. The log message is whatever the individual developer feels like logging at that particular moment in time. So, the format and amount of information varies.
The Right Approach: Use a specified format for the messages. Either create or use a logger that makes the formatting consistent. This can include a logging utility that automatically captures contextual information. You can also write your own reusable code to normalize the message formatting.
Mistake 3: Swallowing Critical Errors/Exceptions
This is probably the most prevalent of all exception-related mistakes. It typically comes out of a misunderstanding of exception handling. It’s also caused by a missing exception-handling strategy in the organization.
The Naive Approach: Wrap everything in try/catch. By wrapping everything in exception handling blocks, we’re trapping exceptions. This isn’t bad in and of itself, but it can often lead to swallowing errors that should be logged and/or rethrown.
The Right Approach: Only wrap code in try/catch if there’s a good reason. One good reason is to log the exception. You could, of course, add detail to the exception and rethrow. Or you could create a new exception with a reference to the original. All of that can be logged at the top of the stack. The log entry will still have the necessary details in the single log entry. Consider the cost and benefits to different approaches, but please don’t just catch an exception and eat it for no good reason!
Mistake 4: Letting Users See Exception Details
This mistake is probably the most alarming of all. Not only does it cause confusion for the average user but it also poses real security risks. Allowing the user to see your stack trace or other specific details about the error means you’re also allowing nefarious users to see inside your system. Those nefarious users can use this information to find attack vectors in your system.
The Naive Approach: Allow exception details to make their way up to the user or API client. This mistake is possibly related to a lack of strategy for error handling.
The Right Approach: Use error codes and custom error pages/responses. Take a proactive approach to error handling and make sure you aren’t passing internal information to the consumer.
Mistake 5: Hard-Coded Configuration Values
The simplest and most common mistake when it comes to configurations is the hard-coded value. This issue has plagued development teams for a long time, and it isn’t going to stop now. This is often used as a shortcut to doing the right thing. At times, it’s due to pure ignorance. Let’s take the case of a maximum length for a string field as an example.
The Naive Approach: Set the length of the input using a constant. This approach has several issues, but we’ll stick to the use of a constant since this is all about hard-coded values. The problem is that the max length can change at any time. The hard-coded value will need to be updated each time. Often, this needs to be updated in many places for a change to one field: the UI code, the database field, the backend code, etc.
The Right Approach: Use a configuration to define the maximum lengths of fields. The configuration approach allows you to change the lengths in a centralized location and without redeploying code. All of the places that use the configured value will be updated at the same time.
Mistake 6: Primitive Feature Flag Management
Feature flags, also known as feature toggles, turn features on and off. You can do this for all users or a subset of users. They’re used for a variety of reasons, such as A/B testing, the slow introduction of new features, and simply increased control over deployment. While it may be a good step to include feature toggles in your applications, you’ll want to be sophisticated in how you control these toggles.
The Naive Approach: Use your application configuration to control feature toggles. With this approach, you add additional entries in the configuration file for each feature and update the configuration to control the features. You’ll have to build out your own management system to get anything sophisticated, and the configuration files will eventually become bloated.
The Right Approach: Use a feature flag management system. Like any argument for buy vs. build, this comes down to using something that’s already solved the problems you’ll encounter. The alternative is to spend valuable time and resources reinventing things. A good feature flag management system gives you the appropriate level of control over your feature toggles. You can manage them centrally rather than having to hunt through configuration files.
Mistake 7: Relying Directly on a Configuration Provider
You need to use configuration values in your code. Most development platforms have some way to get configuration values from the environment and/or config files. Don’t assume you should use the built-in method directly in your code.
The Naive Approach: Use a configuration provider directly in your business logic. This causes a coupling to a specific platform-dependent configuration approach.
The Right Approach: Pass a settings object to your code from the runtime context. Alternatively, you could abstract the configuration provider. This is the same concept, and it’s used for the same reasons dependency injection is—greater flexibility. Another benefit is the explicitly defined config properties that are easier for your code consumers to understand.
Mistake 8: Coupling to Dependency/Platform
Nothing grinds my gears more than seeing platform-dependent code scattered throughout the core of a system. Unless the point of the system is specifically to extend the platform, this kind of coupling has no place in your code.
The Naive Approach: Use platform-dependent code or reference a library/package directly in your code. In this case, you’ll have references scattered throughout your code. Coupling like this will add cost to change platforms or dependencies.
The Right Approach: The right way is to use platform-independent code and adapters for dependencies. Your code should depend on an abstraction rather than something concrete.
Mistake 9: Overengineering
Overengineering causes longer development times and leads to mistakes due to complexity. You may think you’re saving time by designing for contingencies. But in reality, you’re just borrowing trouble.
The Naive Approach: Throw every engineering trick at every problem. When things are simple, you design for every possible complexity. And you build everything for maximum reuse and start by making a framework.
The Right Approach: Start with the simplest thing that works. Use continuous improvement, code reviews, and constant refactoring in order to improve the design. Always keep it simple! If you’re modeling first, refactor the model until it’s as simple as possible. This’ll lead to a clearer path to the best design.
Mistake 10: Missing Types/Primitive Obsession
At some point, you need to stop using primitives for everything. A number is rarely just a number, and a string isn’t just a string. Usually, you’ll have some limitations on numbers and strings. And these two types are really just the tip of the iceberg!
The Naive Approach: Use primitives for every property and attribute of a class/type, and put any logic outside of the attribute, scattered throughout the code. This approach leads to a number of issues including misconfiguration, additional code changes needed for simple requirements changes, and even security/privacy risks.
The Right Approach: Design and use types to contain any logic applied to a field. This will centralize logic as well as contain the data. Here are some example use cases for custom types:
- Apply data protection to mask sensitive data in your logs.
- Change length requirements in one place or through configuration.
- Swap validation strategies as needed.
Keep On Making Mistakes and Learning!
Keep making mistakes. Just don’t keep making the same ones. New mistakes are a byproduct of growth; repeated mistakes mean stagnation. Learn from your mistakes and move on.