We’ve defined the problem, we’ve established the rules, now we are ready to actually play the game. In other words: We will employ consistent and sufficient exception management in an ASP.NET application. No less!
Note: What follows is merely a sample implementation, just to drive the ball home, but it should turn my earlier musings into a little more practical advice. And since this is only meant to highlight the principle, not to provide a production ready solution, I’ll keep it simple.
Exception classes
First let’s try to classify our exceptions, depending on the supposed effect. This classification is sort of the driving force behind the strategy: who is responsible, or more to the point how to tell the responsible people…
A quick reminder on what our goal is:
- The user has to solve everything that results from his user user interaction in due process. This includes invalid input, insufficient permissions, or other business conditions that interrupt the current functionality.
- The administrator is responsible for infrastructure issues. This includes every call into the „outside world“, such as databases, services, file system. Issues here are manifold, including:
- unavailable database or service
- errors during some operation, e.g. unexpected a constraint violations
- erroneous configuration
- Everything else: Bugs. Developer. You! 😉
So consequently:
- If the user is responsible, we just tell him on the page and let him choose what to do.
- If the administrator is responsible, we show a general error page, telling the user that the application is not available, and of course we write some event log entry.
- If the developer is responsible, we do the same, but we tell the administrator explicitly to forward the event log information to the development team.
I’ll spare you the boilerplate exception implementation details, suffice it to say that we have a UserIsResponsibleException for “user exceptions” and a AdminIsResponsibleException for “admin exceptions”, respectively. Names that wouldn’t make it into my production code, but that’s what their intention is, anyway. Oh, and add a ConcurrencyException exception for respective issues, they may or may not be user exceptions, depending on the context.
Now let’s put the handlers in place. These reside in the UI layer, and from there we can work through the architectural layers towards the databases.
User error handlers
User exceptions have to be handled on the page level. Whenever you call into some business layer method, say from a button click event handler, you wrap the call in a try/catch(respective exceptions). The exception is then presented to the user, and eaten, meaning it is not thrown along. Something like this:
protected void btnConcurrentUpdate_Click(object sender, EventArgs e)
{
try
{
BusinessLogic bl = new BusinessLogic();
bl.UpdateDatabaseWithConcurrencyIssue("some content");
lblConcurrentUpdate.Text = "did it.";
}
catch (UserIsResponsibleException ex)
{
// just give feedback and we’re done
PresentError(ex);
}
catch (ConcurrencyException ex)
{
// just give feedback and we’re done
PresentError(ex);
}
}
The way of presenting the error message is something that has to be decided per application. You could write the message to some status area on the page, you could send some java script to pop up a message box. You could even provide a validator that checks for a recent user exception and works with the validation summary for the feedback.
Statistics:
- Exception classes: Write once.
- Presentation: Write once.
The try/catch code is simple enough, yet you have to do it again and again, and should the need for another user exception arise (e.g. some user errors can be handled on the page, some others require leaving the page), the logic is spread across our pages. Thus it makes sense to factor the actual handling into a separate method, in a helper class, or a page base class. Now, catching specific exceptions is still part of our job and that cannot be factored out. To solve this we have three options:
- Build the exception hierarchy accordingly and catch the base class for user exceptions.
- Catch all exceptions and throw the unwanted exceptions on.
- Live with the problem.
I decided against option 1 because I didn’t want the concurrency exception to be derived from some user exception. Option 3 is what we wanted to avoid in the first place. So my solution is number 2, in the name of ease of use and maintenance, but at the expense of language supported exception handling features:
protected void btnConcurrentUpdate_Click(object sender, EventArgs e)
{
try
{
BusinessLogic bl = new BusinessLogic();
bl.UpdateDatabaseWithConcurrencyIssue("some content");
lblConcurrentUpdate.Text = "did it.";
}
catch (Exception ex)
{
// since C# does not support exception filters, we need to catch
// all exceptions for generic error handling; throw those on, that
// have not been handled…
if (!HandleUserExceptions(ex))
throw;
}
}
This is boilerplate and unspecific enough to copy&paste it around. The generic handler may look like this:
bool HandleUserExceptions(Exception ex)
{
if (ex is ConcurrencyException)
{
// just give feedback and we’re done
PresentError(ex);
return true;
}
if (ex is UserIsResponsibleException)
{
// just give feedback and we’re done
PresentError(ex);
return true;
}
// other errors throw on…
return false;
}
Statistics:
- Handler method: Write once.
- Catching exceptions: Every relevant location, boilerplate copy&paste
Please note that exception filters might make this a little less coarse, but this is one of the rare occasions, where a CLR feature is not available to C# programmers. Anyway, we just factored all logic into a centralized method which achieves exactly what we asked for.
But wait… . Wouldn’t the Page.Error event provide a better anchor? It’s already there and no need to add try/catch around every line of code. Look how nice:
protected void btnConcurrentUpdate_Click(object sender, EventArgs e)
{
BusinessLogic bl = new BusinessLogic();
bl.UpdateDatabaseWithConcurrencyIssue("some content");
lblConcurrentUpdate.Text = "did it.";
}protected override void OnError(EventArgs e)
{
// raise events
base.OnError(e);
var ex = Server.GetLastError();
// if we don’t clear the error, ASP.NET
// will continue and call Application.Error
if (HandleUserExceptions(ex))
Server.ClearError();
}
Unfortunately at the time the error event is raised, the exception will already have disrupted the page life cycle and rendering has been broken. It may help in case you need a redirect, but not if the page should continue to work.
“Other” error handlers
So, the page handles user exceptions, what about admin and system exceptions? The page doesn’t care, so they wind up the chain and ASP.NET will eventually pass them to the application, via the Application.Error event, available in the global.asax. This is the “last chance exception handler” or LCEH for short.
One thing we did want to do was writing the exception into the event log. I’ll leave actually writing into the event log to you, just remember that creating an event log source requires respective privileges, e.g. running as admin. Just a minor deployment hurdle.
protected void Application_Error(object sender, EventArgs e)
{
Exception ex = Server.GetLastError();
// unpack the exception
if (ex is HttpUnhandledException)
ex = ex.InnerException;
Session["exception"] = ex;
// write detailed information to the event log…
WriteInformationToEventLog(ex);
// tell user something went wrong
Server.Transfer("Error.aspx");
}private void WriteInformationToEventLog(Exception ex)
{
// an ID as reference for the user and the admin
ex.Data["ErrorID"] = GenerateEasyToRememberErrorID();
string whatToDoInformation;
if (ex is AdminIsResponsibleException)
whatToDoInformation = "Admin, you better took care of this!";
else
whatToDoInformation = "Admin, just sent the information to the developer!";
// put as much information as possible into the eventlog,
// e.g. write all exception properties into one big exception report,
// including the Exception.Data dictionary and inner exceptions.
// also include information about the request, the user, etc.
…
}
As you can see, there are some details to keep in mind. One, the exception has been wrapped by the ASP.NET runtime, and that exception class doesn’t tell you anything worthwhile. Second, passing information to the error page may be a little tricky in cases where the request has not yet (or not at all) established the session state. The easier approach is to let ASP.NET handle the redirection, yet at the loss of information. Three, be careful regarding the requests: depending on the IIS version you may get only .aspx requests, or any request including static resources. Thus, with this simplistic implementation a missing image will send you to the error page depending on the IIS version and registered handlers.
I would also like to encourage you to spend some time on collecting information for the event log message. A simple ex.ToString() doesn’t reveal that much information if you look closely. You should invest some time to add information about the current request (URL, etc.), the user (name, permissions), and server state (app domain name, to detect recycling issues). And use reflection to get all information from the exception, especially including the Exception.Data dictionary. Also go recursively through the inner exceptions. The more information you provide, the better.
I also hinted at an error ID. It is probably a good idea to give the user some ID to refer to when he talks to the admin. This way, the admin will know exactly what to look for in the event log (especially in multiple event logs of a web farm).
Finally, the exception class tells us whether we should include an “Admin, do your job!” or “Kindly forward this issue to your fellow developer, brownies welcome, too.” message.
And by the way: This is the one and only location in our application that writes to the event log!
Statistics:
- Global error handler: Write once.
Done. We have the handlers in place. Handlers for user exceptions, admin issues, and any other exception that might be thrown. Now we need to take care that error causes are mapped to the respective exception class.
Business layer
The business logic is being called from the UI layer, and calls into the data access layer. Thus it has to check parameters coming in, as well as data handed back from the data source.
Data coming in hast to comply with the contract of the business class, and I’d like to stress the fact that not every data is legal. Meaning the business logic is not expected to accept every crap the UI layer might want to pass. A search method that takes a filter class as parameter? No harm in defining this parameter mandatory and require the UI to pass an instance, even if it is empty. And a violation is not the user’s problem, it’s the programmer’s fault! In these cases we throw an ArgumentException, and that’s it. The LCEH will take care of the rest, blaming the UI developer that is.
public IEnumerable<Customer> SearchCustomer(SearchCustomerFilter filter)
{
// preconditions have to be met by the calling code
Guard.AssertNotNull(filter, "filter");
// input values have to be provided by the user
if (string.IsNullOrEmpty(filter.Name))
throw new UserIsResponsibleException("provide a filter value");
DatabaseAccess db = new DatabaseAccess();
return db.SearchCustomer(filter);
}
Any issue with the content of the data, i.e. the information the user provides, is a user issue. Usually this includes data validations and if you follow the rule book you’ll have to repeat everything the UI already achieved via validators. Whether you do that or just provide additional checks, e.g. dependencies between fields, is something I leave to you. Anyway, throw a user exception if the validation fails.
Calling the data access layer may also result in thrown exceptions. Usually there is no need to catch them at all. Period.
Finally the data you get from the database may ask for some interruption. A customer database may tell you that this particular customer doesn’t pay his dues, thus no business with him. In this case you throw a user exception in order to interrupt the current business operation.
And that’s it.
Statistics:
- Business validations and interruptions: As the business logic demands…
“Uhh, that’s it? And I used to spend so much time on error handlers in my business logic…”. Nice, isn’t it? Just business logic, no unrelated technical demand.
Data access layer
The data access layer is where many exceptions come from. If you call a data source (web service, database, file system, whatever), the call may fail because the service is unavailable. Or it may fail because the called system reports an error (constraint violation, sharing violation, …). It may time out. Some exceptions won’t require any special handling and you can leave them alone. Some need to be promoted to admin or user exceptions, or they may require special handling for some reason. You should translate those exceptions to respective application exceptions. The reason is that you don’t want to bleed classes specific to a data source technology (e.g. LINQ 2 SQL exceptions) into the upper layers, introducing undue dependencies.
Luckily all this tends to be as boilerplate as can be. Call/catch/translate. With always the same catches. So, again, we can provide a “once and for all” (at least per data source technology) exception handler, like this one for the ADO.NET Entity Framework:
public IEnumerable<Customer> SearchCustomer(SearchCustomerFilter filter)
{
try
{
using (DatabaseContainer db = new DatabaseContainer())
{
//…
}
}
catch (DataException ex)
{
TranslateException(ex);
// any exception not translated is thrown on…
throw;
}
}
And the respective translation method:
static void TranslateException(Exception ex)
{
// database not available, command execution errors, mapping errors, …
if (ex is EntityException)
throw new AdminIsResponsibleException("…", ex);
// translate EF concurrency exception to application concurrency exception
if (ex is OptimisticConcurrencyException)
throw new ConcurrencyException("…", ex);
// constraint violations, etc.
// this may be caused by inconsistent data
if (ex is UpdateException)
throw new AdminIsResponsibleException("…", ex);
}
Statistics:
- Translation method: Write once.
- Catching exceptions: Every relevant location, boilerplate copy&paste
Conclusion:
Done. Let’s sum up the statistics:
- Write once (at least partly reusable in other applications):
- Application exception classes
- Last chance exception handler (LCEH)
- Handler method for user errors, including presenting the error
- Exception translation method in DAL
- Boilerplate copy&paste
- Catching exceptions in the UI, call handler
- Catching exceptions in the DAL, call translation
- Application code that still needs a brain 😉
- Business validations and interruptions: As the business logic demands…
And this is all we have to do with regard to exception handling in our application. I’ll let that speak for itself… .
This post concludes this little series about the state of affairs regarding error handling in an application. I do not claim to have presented something particularly new or surprising. Rather the opposite, this or some similar strategy should be considered best practice. The reason to even start this series was the fact that I regularly encounter code that does exception handling … questionably. And I also regularly met people who look at exception handling only in the scope of the current code fragment, not in light of a broader approach that spans the application.
If you need further motivation to employ proper error handling in the first place, look here.
A little more on error management in long running, asynchronous scenarios can be found here.
That’s all for now folks,
AJ.NET
Leave a Reply