"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 < 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 && 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 :
Post a Comment