The rule is, of course, that your web site will always work fine, and no problems of any kind will ever happen. Exceptions to that rule can happen during development, however, and even more important, in a production system. It’s needless to mention the many aspects out of your control, like hardware failure, software crashes, and viruses that can cause your software to work not exactly the way you designed it to work. Even better known are the errors that can happen because of bad (or unexpected) user input data combined with weaknesses in the application logic.
Common and particularly dangerous are the errors that can happen when accessing the database or executing a stored procedure. This can be caused by too many reasons to list, but the effects can show the visitor a nasty error message or keep database resources locked, which would cause problems to all the visitors accessing the site at that time.
Exceptions are the modern way of intercepting and handling runtime errors in object- oriented languages. When a runtime error occurs in your code, the execution is interrupted, and an exception is generated (or raised). If the exception is not handled by the local code that generated it, the exception goes up through the methods in the stack trace. If it isn’t handled anywhere, it’s finally caught by the .NET Framework, which displays an error message. If the error happens in an ASP.NET page during a client request, ASP.NET displays an error page, eventually including debugging information, to the visitor. (The good news in this scenario is that ASP.NET can be instructed to display a custom error page instead of the default one—
you’ll do that by the end of the chapter.)
On the other hand, if the exception is dealt with in the code, execution continues normally, and the visitor will never know a problem ever happened when handling the page request.
The general strategy to deal with runtime exceptions is as follows:
• If the error is not critical, deal with it in code, allowing the code to continue executing normally, and the visitor will never know an error happened.
• If the error is critical, handle it partially with code to reduce the negative effects as much as possible, and then let the error propagate to the presentation tier that will show the visitor a nice looking “Houston, there’s a problem” page.
• For the errors that you can’t anticipate, the last line of defense is still the presentation tier, which politely asks the visitor to come back later.
For any kind of error, it’s good to let the site administrator (or the technical staff) know about the problem. Possible options include sending details about the error to a custom data- base table, to the Windows Event log, or by email. At the end of this chapter, you’ll learn how to send an email to the site administrator with detailed information when an error happens.
In our data access code, you’ll consider any error as critical. As a result, you’ll minimize potential damage by closing the database connection immediately, logging the error, and then letting it propagate to the presentation tier.
■Note The business logic you see in the business tier code can control which exceptions pass through it.
Any exception generated by the data access code can be caught and handled by the business tier. In case the business tier doesn’t handle it, the exception propagates to the presentation tier, where it’s logged once again (so the administrator will know it was a critical error), and the visitor is shown a nice error message asking him to come back later.
So, data access errors that are handled somewhere before getting to the visitor are logged only once (in the data access code). Critical errors that affect the visitor’s browsing experience (by displaying the error message) are logged twice—the first time when they are thrown by the data access code and the second time when they display the error message for the visitor.
The theory sounds good enough, but how do we put it in practice? First, you need to learn about exceptions. Exceptions are dealt with in C# code using the try-catch-finally construct, whose simple version looks something like
try
{
// code that might generate an exception }
catch (Exception ex) {
// code that is executed only in case of an exception // (exception’s details are accessible through the ex object) }
finally {
// code that executes at the end, no matter if // an exception was generated or not
}
You place inside the try block any code that you suspect might possibly generate errors.
If an exception is generated, the execution is immediately passed to the catch block. If no exceptions are generated in the try block, the catch block is bypassed completely. In the end, no matter whether an exception occurred or not, the finally block is executed.
The finally block is important because it’s guaranteed to execute no matter what happens. If any database operations are performed in the try block, it’s a standard practice to close the database connection in the finally block to ensure that no open connections remain active on the database server. This is useful because open connections consume resources on the data- base server and can even keep database resources locked, which can cause problems for other concurrently running database activities.
Both the finally and catch blocks are optional, but (obviously) the whole construct only makes sense if at least one of them is present. If no catch block exists (and you have only try and finally), the exception is not handled; the code stops executing, and the exception prop- agates to the higher levels in the class hierarchy, but not before executing the finally block (which, as stated previously, is guaranteed to execute no matter what happens).
Runtime exceptions propagate from the point they were raised through the call stack of your program. So, if an exception is generated in the database stored procedure, it is immediately passed to the data access code. If the data tier handles the error using a try-catch construct, then everything’s fine, and the business tier and the presentation tier will never know that an error occurred. If the data tier doesn’t handle the exception, the exception is then propagated to the business tier, and if the business tier doesn’t handle it, the exception then propagates to the presentation tier. If the error isn’t handled in the presentation tier either, the exception is finally propagated to the ASP.NET runtime that will deal with it by presenting an error page to the visitor.
There are cases when you want to catch the exception, respond to it somehow, and then allow it to propagate through the call stack anyway. This will be the case in the BalloonShop data access code, where we want to catch the exceptions to log them, but afterward we let them propagate to higher-level classes that know better how to handle the situation and decide how critical the error is. To rethrow an error after you’ve caught it in the catch block, you use the throw statement:
try
{
// code that might generate an exception }
catch (Exception ex) {
// code that is executed only in case of an exception throw ex;
}
As you can see in the code snippet, exceptions are represented in .NET code by the Exception class. The .NET Framework contains a number of specialized exception classes that are generated on certain events, and you can even create your own. However, these topics are out of the scope of this book.
See the C# language reference for complete details about using the try-catch-finally construct. In this chapter, you’ll see it in action in the data access code, where it catches potential data access errors to report them to the administrator.