Not All Exceptions Are Created Equal
On ambiguity and how it can bite you where you least expect
This is the sort of pitfall that once you fall into, it doesn’t take long to figure out the solution and then it quickly becomes second nature, you don’t really think about it anymore in the course of the years. It just sticks to the back of your head. Recently I switched jobs back to my previous employer after a couple of years away and was soon put in charge of code reviews which brings us to the aforementioned pitfall that I recently caught in a code review. So I thought I’d take it out of the back of my head and write it down as a warning sign so others don’t fall into this trap.
The problem is a simple one: when you write e.g. a backend service, there are situations where you need to throw an exception somewhere deep in the method/function call chain, and catch it in the outermost layer in order to return some specific error to the client. For example, say you have a REST API endpoint for posting a purchase order. If your service is written in e.g. C# chances are you have an ASP.NET API controller serving that request. Suppose you need to return an error to the API client when a product that is being purchased is out of stock. Since you’re a good programmer with good habits, you probably designed your backend code after some architectural pattern like The Clean Architecture, or Ports & Adapters, or whatever, in other words you didn’t simply stick the entire order purchase code into the API controller. This means when your API controller handles the purchase order request it calls some PurchaseOrders.PostOrder()
method which calls another method, and so on, until some method down the call chain checks if the requested products are available in stock. At this point if some product is out of stock you’d probably throw some exception that bubbles up to the API controller where it gets caught and translated to a proper error response back to the client, like e.g.
HTTP/1.1 400
Content-Type: application/json
{
"errorMessage": "Product {productId} is out of order"
}
In my early days as a novice developer I’d likely throw some exception already defined in the .NET Framework, an exception that somehow relates to the kind of error that I needed to return to the client. For some silly reason I was reluctant to define new exception classes. This is where the trouble begins. That exception was usually System.ArgumentException
, because in my mind this was a kind of invalid input error, right? Maybe…? Anyway, it seemed to make sense to me at the time. The problem with this approach is that this exception is thrown by a multitude of methods from the .NET class library, so if there’s a bug in the code that causes any of those methods to throw an ArgumentException
, your client ends up incorrectly receiving a “product out of order” error. Clearly this exception now has ambiguous meaning in your code. Depending on the situation, it doesn’t mean what you think it means.
Exceptions like ArgumentException
are not inherently bad, but there are a few problems with using it to handle domain-specific errors. First, its meaning is just too broad, and that combined with the fact that it’s thrown by too many classes in the .NET Framework makes it practically impossible to disambiguate bugs from domain errors, which is our primary concern here. Second, like certain other exceptions it’s the kind of exception that’s not designed to be caught. You’re supposed to let it blow as an indication that there is a bug in your code. Third, it doesn’t properly encode domain-specific errors like the product out of order situation in the example above, therefore it does a poor job conveying (or doesn’t at all) the meaning of the error within the context of the business domain, making the code a bit harder to understand.
The problem is a simple one, as is the solution. We need to eliminate the ambiguity, and we can do so by creating a specific exception class for the use case at hand. Something like ProductOutOfOrderException
would suffice. Not only this exception is a lot more specific, it’s also domain-specific, so not only it prevents our API from returning the wrong error to our client, it also conveys domain-specific meaning, making our code a bit clearer and easier to understand. Could I make it a little less specific, like PurchaseOrderException
? Possibly yes, and then you could use the exception message in the error response to the client. As long as it doesn’t somehow become ambiguous, you’re fine.