Monday 5 March 2012

Exception Handling best practices in N-Tier Asp.net applications

"How do you manage Exceptions in your Asp.net applications?"

Pretty simple question, but not that much simple to answer.

We are good at making things. But, may be, we are not equally good at designing systems which properly handles errors with gracefulness, provides user with a polite message about the error and doesn't leave him/her in a dead-end, and internally, notifies the system developers with enough details so that the poor developers don't feel like they need to learn some rocket science to fix those errors.

If you ask me how do I define a good Exception management for an Asp.net application, I would tell you the followings:

Your system has a good exception management if:

  • It doesn't show unnecessary technical error descriptions when an error occurs, rather, apologize to user with a screen that something went wrong and lets him/her go back to the system.
  • When an error occurs, it immediately notifies technical teams with detailed information for troubleshooting, along with logging error details.
  • It has exception management done in a central and manageable manner without unnecessary try..catch...throw spread across the overall code base.

So, if we want to ensure a good exception management in our Asp.net application, we need to meet these three high level objectives.

The bare minimum thing you should do

If you are the laziest developer in the world (Like what I was a few years ago), you should at least take advantage of what Asp.net offers you to handle exceptions gracefully. All you need is to perform the following two simple steps:

Enable customError in web.config:

?
1
2
<customerrors defaultredirect="Error.aspx" mode="On">
</customerrors>

As you might know already, this little configuration instructs the Asp.net run time to redirect to Error.aspx whenever an error occurs in your Asp.net application. Setting mode="On" instructs to redirect always, which may not be a good choice if you are developing your system. Setting mode="RemoteOnly" should be perfect choice for you as this results in redirection to the error page only when page is browsed from a remote machine.

Define an error page:

You obviously need to create an error page, right? The Error.aspx page you may like to have is as follows:


Figure : The simplest error page

So, I created the Error.aspx as follows, in my Asp.net web site created using Visual Studio 2010:

Markup:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeFile="Error.aspx.cs" Inherits="Error" >
<asp:content contentplaceholderid="HeadContent" id="HeaderContent" runat="server">
</asp:content>
<asp:content contentplaceholderid="MainContent" id="BodyContent" runat="server">
    <asp:panel id="pnlError" runat="server" visible="false">
        <asp:label id="lblError" runat="server" text="Oops! An error occurred while performing your request. Sorry for any convenience."></asp:label>
         
 
        <asp:label id="lblGoBack" runat="server" text="You may want to get back to the previous page and perform other activities."></asp:label>
         
 
        <asp:hyperlink id="hlinkPreviousPage" runat="server">Go back</asp:hyperlink>
    </asp:panel>
</asp:content>

CodeBehind:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public partial class Error : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Page.Title = "Error occurred";
 
        string PreviousUri = Request["aspxerrorpath"];
 
        if (!string.IsNullOrEmpty(PreviousUri))
        {
            pnlError.Visible = true;
            hlinkPreviousPage.NavigateUrl = PreviousUri;
        }
    }
}

After performing these two steps, whenever an unhandled exception occurs now, Asp.net run time will redirect the current request to the error page (In this case, Error.aspx) with a query string parameter "aspxerrorpath" with the value being set with the previous page URL. So, Error.aspx.cs is able to read the previous page URL from this request parameter and display a graceful error message with a "Go back" hyperlink to let the user go back to the previous page.

To test whether it works or not, I created an Exception scenario as follows:

User browses Default.aspx and clicks on "Do Something"button.
The Button click event handler method throws an Exception.
Asp.net run time catches this Exception and redirects the user to the Error page.

It works pretty fine as expected.

Who will fix the error?

Very important question. Its good that your system now gracefully handles exceptions. But this isn't enoug, right? We need a mechanism to catch the exceptions and notify our technical team or log the exception details somewhere so that they can analyze and fix the error as early as possible.

So, we need a way to capture the Exceptions and do something of our own. Let's see how we can do that.

Page.OnError()event method on CodeBehind class

Each Asp.net Page CodeBehind class inherits the System.Web.UI.Page class and can override the OnError() event of the base class. Doing this lets the page capture any unhandled exception using the following piece of code:
?
1
2
3
4
protected override void OnError(EventArgs e)
{
        Response.Redirect(string.Format("Error.aspx?aspxerrorpath={0}",Request.Url.PathAndQuery));
}
As we typically have multiple pages in our Asp.net applications, it would be smarter if we define the OnError() inside a base class (Say, BasePage) for all CodeBehind pages as follows:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class BasePage : System.Web.UI.Page
{
    public BasePage()
    {
        //
        // TODO: Add constructor logic here
        //
    }
 
    protected override void OnError(EventArgs e)
    {
        //Report Error
        Exception ex = Server.GetLastError();
        ErrorHandler.ReportError(ex);
        Server.ClearError();
        Response.Redirect(string.Format("Error.aspx?aspxerrorpath={0}",Request.Url.PathAndQuery));
    }
}
This would allow us to get rid of implementing OnError event method on each and every CodeBehind pages.

Application_Error() event in Global.asax

As you might know already, you can define Application level events inside the Global.asax file, which lets you define a global error handler event method Application_Error(). If you define an Application_Error() event method in this file, each and every unhandled exceptions will be captured by this event method and you can do whatever you want. In our case, primarily we would want to report the exception details inside this event method, along with redirecting user to the error page:

1
2
3
4
5
6
7
8
9
void Application_Error(object sender, EventArgs e)
{
        // Code that runs when an unhandled error occurs
        //Report Error
        Exception ex = Server.GetLastError();
        ErrorHandler.ReportError(ex); //Notifies technical team about the error
        Server.ClearError();
        Response.Redirect(string.Format("Error.aspx?aspxerrorpath={0}",Request.Url.PathAndQuery));
}
If you handle exceptions via the Application_Error event handler at Global.asax, you do not need to handle exceptions via the Page.OnError() event handler. So, life gets event easier.

Shouldn't the Exceptions be handled in CodeBehind classes?

This is a good question. One can obviously suggest to handle Exceptions in the CodeBehind classes as follows:

1
2
3
4
5
6
7
8
9
10
try
{
    //Do some stuff
    businessObject.SaveObject(object);
}
catch(Exception ex)
{
    ErrorHandler.ReportError(ex); //Notifies technical team about the error
    Response.Redirect(string.Format("Error.aspx?aspxerrorpath={0}",Request.Url.PathAndQuery));
}
But, the CodeBehind classes could have many codes which could invoke methods on some other tiers and, Exceptions could be thrown from those methods. Moreover, there will be many CodeBehind classes. So, in the above manner, the try..catch Exception handling codes has to be written in hundreds of places (Not to forget the fact that, if we need to change in Exception handling mechanism, we also would require to change the codes in hundreds of places). Does this really sound a smart thing to do when we can handle the Exceptions from within a central place (Application_Error())? Absolute no.

So, DO NOT WRITE any try..catch block inside the CodeBehind classes and keep it absolutely free of any ugly error handling mechanism (Well, there may be an exception to this suggestion, will see shortly). Utilize the power of Asp.net and keep your Exception handling mechanism simple and manageable from a central place.

I have an N-Tier Asp.net application. How should I handle exceptions?

This is a classic question. Multi-tier applications are pretty common these days, and, what should be a proper exception handling policy to follow in such applications? Should we handle exception in each tier using try...catch blocks? Should we throw exceptions from a tier to an upper tier? Should we log exception inside each tier?

I would try to simplify the answers as follows. You should do whatever is required to meet your high level objectives. That is, error reporting to the technical team and graceful exception handling to the end-user. You also need to make sure that you are not writing some repeating codes to handle the exceptions in multiple places.

So, let us try to derive an exception handling policy for an N-tier Asp.net application by analyzing our high level objectives, and, using some common sense.

We need to handle Exceptions gracefully

It doesn't matter how many tiers we have in our Asp.net application. As long as we have an Application_Error event defined in Global.asax, nothing can escape it. So, no matter where an Exception occurs in our multi-tier application, the Exception will be captured in Global.asax and it would do whatever it has been instructed to do (Redirect user to an error page).

We need to notify Exception details to technical team

Yes, the Application_Error event method also reports exception details to the technical team via some mechanism. But, wait a minute! Should we really need to notify the technical team about each and every exception? may be not.

What are the exceptions we need to report to the technical team?

Suppose, a Data Access Layer method is being invoked in the DAL layer (A separate dll) within our application, and, the DAL method may throw an exception from the database. Should we report such Exceptions from the Data Access Layer to the technical team? Obviously yes.Why? Because the error has to be fixed in the Database routines.

If any unhandled Exception (Say, a NullReferenceException) is being thrown from any part of the codes (Be it generated from within the CodeBehind classes, or, from any other tiers) should it be reported to the technical team? Obviously yes. Why? For the same very reason that the error has to be fixed in the codes with some good defensive programming.

Now, suppose we are taking an input from the user and invoking a business method with the input parameters. The business method expects a particular parameter value to be within a certain range. If it doesn't fall into that range, it throws an Exception with an appropriate message. Should we report this kind of exceptions to the technical team? Obviously not. Why? Because, this exception is usually thrown with an intention to let user know that he has done something wrong and the exception message is used to guide him/her to do the right thing.

Also, suppose a ThreadAbortException is being generated as a result of using Response.Redirect() in the CodeBehind classes. Should we notify the developers about this exception after capturing inside the Application_Error event handler? Obviously not. Why? Because, this particular exception is not being generated as a programmatic or system failure, and, there is nothing to fix.

So, in a multi-tier application Exceptions could be generated from each tier, and, these exceptions could be of different general categories, and, based upon their category these Exceptions need to be handled in different approach. Some particular category of exceptions are needed to be reported to some particular technical teams, some particular category of exceptions need not to be reported to any technical team, rather, these are to be used to guide user to use the system correctly, and, some particular category of exceptions just needed to be ignored. So, this generates a need to develop an Exception hierarchy which could be used commonly across all different tiers within the application.

Exceptions generated from each tier would be categorized according to their corresponding general type (When an Exception occurs, this would be done by wrapping an Exception with a particular type of Exception from the Exception hierarchy) and will be thrown to the upper tier so that, an exception management policy can determine the category of the exceptions and handles those judiciously.

The custom exception hierarchy

Following could be a very basic Exception hierarchy that could be used to wrap up the original exceptions and categorize them so that, these exceptions could be handled using a good exception management policy.

Figure : A basic custom Exception hierarchy

The Exception class names are self-explanatory. They all inherits a base exception class BaseException which itself inherits the Exception class.

So, this Exception hierarchy may be used as follows:

Inside DAL methods:

Catch any Exception that might get thrown by invoking database operations, wrap it inside a DALException, and throw to the upper tier.

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public List<User> GetUsers()
{
    string SP = AppConstants.StoredProcedures.GET_ALL_USERS;
 
    IDataReader reader = null;
    List<User> users = new List<User>();
    try
    {
        using (DbCommand dbCommand = Database.GetStoredProcCommand(SP))
        {
            using (reader = Database.ExecuteReader(dbCommand))
            {
                while (reader.Read())
                {
                    User user = User.CreateFromReader(reader);
                    users.Add(user);
                }
            }
        }  
    }
    catch(Exception ex)
    {
        throw new DALException("Failed to retrieve User.", ex);
    }
    return users;
}

Inside business methods:

Generally, we need not to handle Exception inside the business methods while calling this (Or any) DAL methods. Why? Because, the DAL methods are already handling exceptions. So, whenever any exception occurs while invoking a DAL method from the business methods, the exception will be propagated to the call hierarchy across the upper tiers, and will be caught by the global exception handler Application_Error().

So, you just invoke the DAL method as follows in one of your business methods:
1
2
3
4
5
6
7
8
9
10
11
public List<User> GetUsers()
{
    UserDAO userDAO = new UserDAO();
    List<User> users = userDAO.GetUsers();
    if(users != null)
    {
        //Do other stuffs
    }
    //Do other stuffs
    return users;
}
But, as has been said already, we may need to throw some custom exceptions inside a few business methods as follows (To let the caller notify that a certain condition has to be met in order to proceed the operation):
?
1
2
3
4
5
6
7
8
9
public void Add(User user)
{
    if(user.Age &lt; 18)
    {
        throw new BLLException("Not allowed for kids!");
    }
    UserDAO userDAO = new UserDAO();
    userDAO.Add(user);
}
Inside CodeBehind classes

Only Exceptions of type BLLException are to be handled here. Why? because, BLLException actually represents a business logic validation error (With a validation error message, see above example) which is needed to be displayed to the end-user to guide him/her to provide correct input or carry out his/her operations correctly. If we do not handle BLLException in the CodeBehind classes, these exceptions will be propagated to the global error handler Application_Error and it will either ignore the exception, or, report the exception to the technical team, neither of which is desired.

So, following is how we may handle the BLLExceptions in CodeBehind classes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected bool AddUser()
{
    UserManager userManager = new UserManager();
    User user = PopulateUserFromInput();
    try
    {
        userManager.Add(user);
    }
    catch(BLLException ex)
    {
        ShowErrorMessage(ex.Message);
        return false;
    }
    return true;
}
Inside Application_Error() event in Global.asax

Well, when an Exception is bubbled up to this global exception handler method, it could either be a ThreadAbortException, or, a DALException, or, a BLLException or, an unhandled exception for which we didn't catch it using any try...catch block inside our Application.

This Application_Error event handler method would be interested to handle only DALException and any unhandled exceptions. As we know already, it would report the exception details (Probably will send emails about the exception to some pre-configured email addresses, and, log the exception details somewhere). It would simply ignore the ThreadAbortExceptions (Because, nothing to be fixed) and BLLException (Because, this is actually a custom exception which should be handled inside the CodeBehind classes to show validation message to the user).

This is how the Application_Error event method could look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Application_Error(object sender, EventArgs e)
{
    Exception ex = Server.GetLastError();
 
    if (ex as System.Threading.ThreadAbortException != null &amp;&amp; ex as BLLException != null)
    {
        //Do not handle any ThreadAbortException or BLLException here
        //Only handle other custom exception (DALException in this example)
        //and any other unhandled Exceptions here
 
        ErrorHandler.ReportError(ex);
        Response.Redirect(string.Format("Error.aspx?aspxerrorpath={0}", Request.Url.PathAndQuery));
    }
}
Asp.net and Microsoft Exception Handling Application Block

You can use the Exception handling Application Block provided my Microsoft, which can suit your need perfectly. You can define exception policy for each kind of exception in declarative manner, and, you can configure the block to how to act to each kind of exception. Once installed, you can configure the Application Block using GUI tools right within the Visual Studio.

This link could help you configuring the Exception Handling Application Block in your Asp.net application. I found it handy.

Conclusion
Remember these simple rules while you design your Exception handling mechanism in your N-Tier Asp.net applications:
  • Define a Global exception handler, report exception details to the technical team via an exception reporting mechanism (Probably with sending emails and logging the exception) and redirect the user to an Error page with a polite message and provide him/her link to get to the original page.
  • Categorize the exceptions using an exception hierarchy which suits your need and define a well-thought out policy to deal with each category of Exception.
  • Let all exceptions be propagated to the global exception handler, except the custom exceptions (BLLException in this example) which are meant to be showing an error message to the user to guide him/her carrying out his/her works correctly.
  • Once an exception is thrown (Be it thrown by you or by the CLR) from any tier from within your N-tier application, catch it only at a single place (Be it at CodeBehind classes or global exception handler) and DO NOT re-throw it after you catch. Throw once, Catch once.
Remember, plan before you do. Laying out a clean well thought-out Exception handling mechanism in your N-tier Asp.net application would let you run your system smooth, and let you know about any error that is to be fixed immediately when it happens. The end result is, your development team, management and the client, everybody is happy.

No comments :