Introduction
If you have a Domain Model, you're going to have code that validates rules and checks for inconsistencies. Methods in the Domain Model are going to have to indicate failure in some way (often corresponding to an exception scenario in a use case).
My personal preference is always to start with the simplest way of handling business logic errors: throw an exception.
Avoid Return Codes
We know that the wrong way to indicate a problem is by using an error return-code from each function. The call-stack can be quite deep, and you cannot (and should not) rely on the callee of each function to check each return value.
We also know that failing fast is (perhaps unintuitively) the way to write more stable applications.
Exceptions are a language mechanism that is perfect for indicating that something has failed. The semantics allow you to 'get the hell out of Dodge' without having to worry about handling the failure, or how deep in the call stack you are. If the exception is not handled correctly, it will expose the problem in an obvious way.
Additionally, throwing an exception means that you don't have to worry about the code that would have been executed next. This can be of advantage where:
- The subsequent code would otherwise have failed (e.g., if the validation found that a string was not unique and inserting it into a database would cause a unique-key violation);
- The subsequent code used a non-transactional resource (e.g., a synchronous external web service, or the file system.)
Catching Exceptions in the Presentation
When you're handling exceptions in the Presentation layer you have an opportunity to concisely handle the specific scenarios you know about while elegantly ignoring any others:
public void CreateButton_OnClick()
{
try
{
library.CreateBook( View.BookTitle,
View.BookAuthor,
View.BookDescription);
}
catch (BookTitleNotUniqueException)
{
View.ShowMessage("The supplied book title is already in use");
}
}
The above try-catch construct provides a way to indicate that there is a programmed response when the supplied name is not unique. More importantly, it indicates that any other error should automatically be propagated up the call chain.
Service Layer
Many applications still separate the presentation from the domain with a (remote) Service Layer. However, most remoting technologies (e.g., WCF Web Services) don't play well with exceptions.
Typically the Service Layer will catch the exception, roll-back any transaction, and use a custom DTO or framework-provided mechanism to pass the details of the exception the to client. (WCF supplies the FaultContract class for this)
The DTO should be turned back into an exception on the client-side (using AOP/interception) so that it will still be propagated up the call chain if it is not handled. When using WCF it is advisable to convert the FaultException object back into a regular exception to prevent coupling your presentation layer to WCF.
Where you have no Service Layer, you'll probably need to call a method (probably in the constructor of your custom exception class) to indicate to any transaction handler that the transaction should be rolled back.
Multiple Failures
A common scenario where failing fast is not immediately suitable is where the client wants multiple validation errors handled in a single call.
In these cases I try to wrap the multiple checks into a fluent interface that throws a single exception that contains a collection of validation errors. The code might look something like:
new ValidationBuilder()
.Add(ValidateAddressLine1())
.Add(ValidateAddressLine2())
.Add(ValidateAddressLine3())
.Add(ValidatePostcode())
.ThrowOnError();
While this is a common scenario to find in Internet applications, (e.g., when filling out your car insurance quote online), it is rarely required for every screen of an average internal LOB application.
I personally try to avoid this until the client asks for it. The logic required to continue the validation code typically becomes harder to write when you can't assume that preceding validation was successful.
Common Arguments
There seems to be conflicting advice on using exceptions, with common advice being to avoid them for anything other than runtime errors in the infrastructure (e.g., disk full, connection broken, etc.) I am not aware of a technical reason for not allowing exceptions to be used for business logic errors.
Others have sophisticated solutions for passing notifications out from the domain that avoid the use of exceptions. I try to avoid resorting to more complicated solutions until they are required - exceptions are a simple mechanism that are easily understood by programmers.
Less interesting arguments, however, usually revolve around performance. Any performance loss from throwing an exception is usually insignificant by comparison to the length of time taken for the business transaction itself.
Summary
Exceptions are the perfect mechanism for reporting inconsistencies in domain logic. I believe judicious use of them will make your applications more stable, and make your code neater and easier to maintain.
Be wary of anyone telling you that this is not what exceptions are for ... this is exactly what they are perfect for!