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.