What Exactly Is An Exceptional Circumstance, Anyway?

I think that there's a general consensus out there that Exceptions should be limited to exceptional circumstances. But being that "exceptional" is a rather subjective adjective, there's a bit of a gray area as what is and isn't the appropriate use of Exceptions.

Let's start with an inappropriate use that we can all agree too. And I can think of no better place to find such an example than TheDailyWTF.com. Although that particular block of code doesn't exactly deal with Throwing exceptions, it is a very bad way of handling exceptions.

To the other extreme, exceptions are very appropriate for handling environment failure. For example, if your database throws "TABLE NOT FOUND," that would be the time to catch, repackage, and throw an exception.

But it's in the middle where there's a bit of disagreement. One area in particular I'd like to address in this post is exceptions to business rules. I mentioned this as an appropriate before, but noticed there was quite a bit of disagreement with that. But the fact of the matter is, exceptions really are the best way to deal with business rule exceptions. Here's why.

Let's consider a very simple business rule: an auction bid may be placed if and only if (a) the bid amount is higher than the current bid, (b) the auction has started, and (c) the auction has not ended. Because these rules (especially b and c) are domain constraints (i.e. they restrict the range of acceptable values), they are best handled by the Preserver of Data Integrity (some call this the "database"). To accomplish this validation, we'll use a stored procedure with an interface like this: procedure Place_Bid ( @Auction_Num char(12), @Bidder_Id int, @Bid_Amt money )

Now let's consider the layers of abstraction it actually takes to go from the user clicking the "Place Bid" button to the stored procedure being called:
•PlaceBidButton_Click()
•AuctionAgent.PlaceBid()
•IAuction.PlaceBid()
•--- physical tier boundary --
•IAuction.PlaceBid()
•Auction.PlaceBid()
•AuctionDataAgent.PlaceBid()
•SqlHelper.ExecuteCommand()
•--- physical tier boundary --
•procedure Place_Bid

Without using exceptions, it gets pretty ugly passing the message "Bid cannot be placed after auction has closed" from the stored procedure all the way back to the web page. Here's two popular ways of doing this:

  • Return Codes - Have every method that could potentially fail return some sort of value. True/False is the most common but rarely provides enough information about the failure. Our PlaceBid function would need four different return codes: Success, Fail-LowBid, Fail-EarlyBid, Fail-LateBid. Of course, this technique fails when your method may need to actually return something other than the return code.
  • Class Level Checking - For each of classes, add property called "LastError." This will contain an Error object that contains information about the last error (if one occurred). Simply check it after each operation.
  • Output Params - Add an out paramter to every method to pass back an ErrorObject. This is similar to the aforementioned technique except it is on the method-level.

In all three cases, you need to manually "bubble" up the message from method to method. As you can imagine, this adds lots and lots of needless "plumbing" code intertwined with your business logic. Since it's at the method level, all it takes is one developer to not code a method to return the right code.

The proper way of handling the Bid exception is, naturally, with Exceptions. When you raise the error in the stored procedure code, indicate that the message is a business rule exception intended to be displayed back to the end user. After that, you only need to put try/catch blocks in two places, the ExecuteCommand() method and the PlaceBidButton_Click() method.

ExecuteCommand() Psedo-code
Try
  sqlCmd.ExecuteNonQuery();
Catch ex As SqlExecption
  If IsUserSqlException(ex) Then Throw ConvertSqlExceptionToBusinessException(ex)
End Try

PlaceBitButton_Click() psedo-code
Try
  AuctionAgent.PlaceBid()
Catch ex As BusinessException
  DisplayUserMessageFromException(ex)
End Try

Less code, less mess. Nanoseconds slower? Probably. A big deal in the context of a web request going through three physical tiers? Not at all.

4 Comments

  • I respectfully disagree with your conclusion. If you're following any decent OOP practices, you will have an instantiated Auction object in your business layer that you can validate before you pass it to your persistance layer.



    Exceptions *are* for "exceptional" situations where you really, as a developer, have almost no control over. One example that pops into mind is a WebException when a server cannot be found. How exactly are you going to know that when calling myWebRequest.GetResponseStream()?



    In your example, all of your business rules can be checked before an exception be thrown. If you allow your exception to be thrown from the persistance layer, you are wasting time man! ;) Just define a new exception and throw it from your validator.



    I know and understand your point, but I don't think it's a very good argument. Sorry that this was short and vague. I don't much time right now.



    Chris

  • create it in the catch if it didn't exist

  • I'd have to back Chris up and say rules like this should be checked in the business layer.



    BradC has raised a point saying that you need to be cautious that other bids aren't received between the time the validation is done and the actual insert - but this is pretty similar to any other concurrency problem.

  • Or, alternatively, you're all right, but none of you is right enough. In the ideal case, the business logic should be enforced in the database as constraints/triggers, *and* the app should validate data before sending it to the database. The key is to achieve this without duplicating business logic.



    This means that you need an implementation-independent representation of the constraints, which can be translated into database constraints by your schema management tool, and object validation rules by your O/R mapper.



    I've not seen such a system, but my company has something a bit similar: a form field validation language that gets translated into two things: (a) rules for validating the form object before persisting it to the database; and (b) dynamically generated Javascript code to do the same validation on the client's web browser. In principle, we could add (c) translating the rules into database constraints, but that's really, really hard to do in a database-independent manner.



    The key here is that each layer of validation is doing different things: the database server is guaranteeing data integrity in the face of non-conformant clients; the app-server validation is saving us a trip to the database, plus it can deal with validation failures in an application-specific way (e.g. displaying a response page that highlights the error fields in red); the client-side Javascript validation is providing the user a faster, more interactive UI (and reducing some load on the app servers).

Comments have been disabled for this content.