Recently my team was working on an API endpoint that had an edge case. The happy path was to simply insert a record. But in rare cases, that record might already exist. The way we had structured the logic, handling this case was slightly tricky. It ended up representing about half of our logic and made the endpoint harder to test.
When chatting with our architect about the best way to handle this edge case we received a surprising answer. Don’t handle it. Instead, restructure your logic so that there is no edge case.
Lots of Edge Case Code Is a Code Smell
If you find yourself writing a lot of code to handle edge cases, it might be a code smell. Of course, we want to create robust code and that means we need to think of the edge cases. At scale, edge cases happen regularly. So my argument is not to simply ignore them.
The question is how we solve a given edge case. In some cases, we must write special code to handle that edge case, but in other cases, we may be able to simply restructure our logic such that the edge case goes away. That is the ideal. Having lots of edge case handling in our code is a code smell because it might indicate that we didn’t take this approach enough.
Example: Null Values
Perhaps you have logic in many places to handle the fact that a certain field might be null. To get more specific, let’s say you have a nullable string stored in a database. Downstream, you might be performing string operations such as
toUpperCase on this value. But since it is nullable, you have to check each time.
Ideas to refactor:
- Must the string be nullable? Perhaps you can put a not null constraint on it. Even if you need an empty value, could it be represented by an empty string?
- You might also consider using a language that supports strict null checks to complement this.
Example: Inserting a Record That Might Already Exist
Let’s say you are inserting a record into a relational database that might cause a unique constraint violation. After doing this, you perform several other operations that all need to be handled differently if the insert caused a violation. The result is that you have lots of branching in your application.
Instead of having all these branches, let’s consider some alternatives.
- Is it necessary to have unique constraints? Perhaps you should store all the versions of this record and then fetch the most recent one when accessing it.
- Can we short circuit our logic somewhere early so we only have one logical branch?
- Does it make sense in your case to use an upsert style query instead of an insert?
Example: Supporting Outdated Clients
Maybe you’re having difficulty getting a specific feature of your app to work in an older browser version. This may be critical for you to support, but make sure you have investigated. Do we need to support this browser version? Can we still meet agreements by supporting it in a mildly degraded state?
Example: Putting a Queue in Front of a Queue
Let’s say we need to publish a message to a queue. What if the publish fails? Should we write the data to another source first? Now you are essentially putting a queue in front of another queue. Won’t the same problems exist in both queues?
In some cases, this may make sense. Maybe the first queue has low reliability and you don’t have control of it. But take time to think. Maybe it is going to end up having nearly the same failure scenarios and probabilities.
Or even if the second queue does reduce your failure rate, is it worth the complexity? What is your acceptable level of risk for the application?
The Google SRE handbook states it well: “We strive to make a service reliable enough, but no more reliable than it needs to be. That is, when we set an availability target of 99.99%, we want to exceed it, but not by much.” (Google SRE, Chapter 3 - Embracing Risk)
Perhaps it would be better to just let it fail and reflect that failure to the user.
Edge cases are important to plan for. We should value the most simple solutions to those edge cases that are reasonable. If half or more of your code is handling edge cases, you should probably rethink how you are handling your edge cases. As with any code smell, this is just a heuristic, not a rule.