HOWTO: Log website errors using exceptions in ASP.NET

Defen­sive pro­gram­ming (ie. check­ing object for null before hav­ing an action on it) can be a great tool but do we always can pre­vent all the errors espe­cially when it comes to user inputs or web servers that not always reli­able. When appli­ca­tion is up and run­ning there are not many ways we can find out that some user had an error unless the error is global so appli­ca­tion goes down. Of course if we have an access to Win­dows Event Viewer on the server we can check it daily but in my hon­est opin­ion Event Viewer is quite hard to use.  Instead of that we can write our own Error­Log­ger. So each time user or server have a bug we will have a nice report on that. For error track­ing we will use .NET built in Excep­tion class and Try and Catch block.

part I — set­ting up database

Cre­ate a data­base with name Error­Log inside of it cre­ate a table in data­base called Errors with fol­low­ing fields errorID, userID, userIP, error­Page, error­Name, errorStack, error­Date and isRead.


CREATE TABLE [dbo].[Errors](
[errorID] [int] IDENTITY(1,1) NOT NULL,
[userID] [int] NOT NULL,
[userIP] [varchar](14) NOT NULL,
[errorPage] [nvarchar](200) NOT NULL,
[errorName] [nvarchar](100) NOT NULL,
[errorStack] [nvarchar](max) NOT NULL,
[errorDate] [smalldatetime] NOT NULL,
[isRead] [bit] NOT NULL,
CONSTRAINT [PK_Errors] PRIMARY KEY CLUSTERED
(
[errorID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

userID will be stored in case you have user logged in ( or any other track­ing para­me­ter ie. user Guid), oth­er­wise you can save zero value. Abil­ity to know what user had an error can help you to debug the appli­ca­tion. userIP field use­ful to know whether it is the same user accessed resource with error in case there was no userID pro­vided. error­Page is use­ful because some­times there is no indi­ca­tion in Exception’s stack where an error appeared so sav­ing it could be a good idea also in case you have one error log­ging sys­tem for mul­ti­ple web­sites this way you will save time under­stand­ing where it was or to review an error log at any web­site. error­Name is a short name of the excep­tion that mostly not descrip­tive enough to debug but use­ful for short indi­ca­tion or error cat­e­gory needs. errorStack field will be used to store every­thing from Exception.ToString(). error­Date obvi­ously for sort­ing and tim­ing issues and isRead is use­ful in cases when you don’t delete errors but just hide them from viewing.

Next lets write a stored pro­ce­dure “LogEr­ror” which will insert an error to data­base, code is quite straightforward.


CREATE PROCEDURE dbo.LogError
@userID int,
@userIP VarChar(14),
@errorPage NVarChar(200),
@errorName NVarChar(100),
@errorStack NVarChar(max)
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO Errors VALUES (@userID,@userIP,@errorPage,@errorName,@errorStack,getdate(),0)
END
GO

part II — the code

We got two options “lazy way” and the “proper way”. The “lazy way” is to cap­ture an error as it appears from global.asax Application_Error() method which i will show you in a few but it is impor­tant to under­stand that the “lazy way” won’t give you much as flex­i­bil­ity as “proper” one sim­ply because you will just log some of errors with­out the abil­ity to inform user of what really hap­pened. Catch­ing an error from global.asax is a global method which will act the same for any kind of error from any kind of page while “proper way” will allow you to have con­di­tional logic so for x error appli­ca­tion will redi­rect z page and y error will redi­rect some­where else and show user friend mes­sage  (ie. if not logged in user tries to get to page that requires userID stored in ses­sion or cookie you would like him to be redi­rected to login page, to show him user friendly mes­sage and then to log an error which could be caused by wrong account con­fir­ma­tion mail you have sent SO IT IS IMPORTANT TO LOG ANY ACTION). This way you will know for sure where, why and how.

The “lazy method”

Code pretty much speaks for itself but here some expla­na­tion. First we get an Excep­tion from Server.GetLastError() object then we con­nect to data­base, with “LogEr­ror” stored procedure

*** “Lazy method” suit­able for those who already have an online appli­ca­tion when archi­tec­tural changes in its design will cre­ate need­less bugs. Even though  “proper method” has much wider flex­i­bil­ity than just log­ging errors, so next time you start another project con­sider using the proper one but to use it on live appli­ca­tion is not such a good idea.

Add this piece of code to Application_Error() method of global.asax.


Exception currentError = Server.GetLastError().GetBaseException();
using (SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["Errors"].ConnectionString))
{
cn.Open();
SqlCommand com = new SqlCommand("InsertError",cn);
com.CommandType = CommandType.StoredProcedure;
com.Parameters.Add("@userID", SqlDbType.Int).Value = Users.getUserID();
com.Parameters.Add("@userIP", SqlDbType.VarChar,14).Value = Users.getUserIP();
com.Parameters.Add("@errorPage", SqlDbType.NVarChar,200).Value = Request.Url.ToString();
com.Parameters.Add("@errorName", SqlDbType.NVarChar,100).Value = error.Name;
com.Parameters.Add("@errorStack", SqlDbType.NVarChar).Value = error.ToString();
try
{
com.ExecuteNonQuery();
Server.ClearError();
Response.Redirect("~/errorpage.aspx");
}
catch (Exception)
{
}
}

This is not a good idea to use empty catch block but we have no choice and nowhere to log this error, think of sit­u­a­tion when your appli­ca­tion data­base in one place and Error­Log on another, by hav­ing try and catch block in there we pro­tect user from being thrown from appli­ca­tion when Error­Log data­base will fail. So we won’t track an error but user will con­tinue to browse our application.

The “proper method”

Lets cre­ate a class inside our App_Code folder caller Errors. Add a method in it caller logEr­ror. This method it wrapped by try and catch block so we will avoid Excep­tion in case we could not be able to estab­lish con­nec­tion to Errors data­base or any other con­nec­tion error like trans­ac­tion dead­lock. Thanks to Shuaib Rameh for point­ing out this mistake.

try {
public static void logError(Exception ex)
{
using (SqlConnection cn = new SqlConnection(ConfigurationManager.ConnectionStrings["Errors"].ConnectionString))
{
SqlCommand com = new SqlCommand("LogError", cn);
com.CommandType = CommandType.StoredProcedure;
com.Parameters.Add("@userID", SqlDbType.Int).Value = Users.getUserID();
com.Parameters.Add("@userIP", SqlDbType.VarChar,14).Value = Users.getUserIP();
com.Parameters.Add("@errorPage", SqlDbType.NVarChar,200).Value = Request.Url.ToString();
com.Parameters.Add("@errorName", SqlDbType.NVarChar,100).Value = ex.Name;
com.Parameters.Add("@errorStack", SqlDbType.NVarChar).Value = ex.ToString();
com.ExecuteNonQuery();
}
}
}
catch(Exception)
{
}

Method is sim­i­lar to one we added to Application_Error han­dler inside of global.asax with with dif­fer­ence we pass Excep­tion as para­me­ter and use its val­ues to track an error. Now lets see how to use this method and what its flex­i­bil­ity will allow us to do.


public static bool isUserEmailExists(string userEmail)
{
bool isEmailExists = false;
try
{
using (SqlConnection cn = DB.connectMainapplicationDataBase())
{
SqlCommand com = new SqlCommand("IsUserEmailExists", cn);
com.CommandType = CommandType.StoredProcedure;
com.Parameters.Add("@userEmail", SqlDbType.VarChar, 50).Value = userEmail;
using (SqlDataReader reader = com.ExecuteReader())
{
if (reader.HasRows)
{
isEmailExists = true;
}
}
}
}
catch (Exception ex)
{
Errors.logError(ex);
}
return isEmailExists;
}

This a sam­ple method in which we check whether user’s email exists in our data­base ie. for pass­word recov­ery needs or reg­is­tra­tion. Con­nec­tion is wrapped up with try-catch block and on Excep­tion we track it down with a method we cre­ate inside of Errors class. We can redi­rect or do what ever we want but error will be logged. Con­nec­tion here is made with using block for more infor­ma­tion about it read  HOWTO: Prop­erly con­nect to MSSQL data­base .

Share and Enjoy:
  • Print
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • DotNetKicks
  • DZone
  • LinkedIn
  • StumbleUpon
  • Technorati
  • Live
  • PDF

Tags: ASP.NET, exceptions

6 comments

  1. Shuaib Rameh

    Hi,
    Great arti­cle… but what hap­pens if excep­tion is type of SQL con­nec­tion?
    Your logEr­ror method throws an new excep­tion. No?
    I think it would be bet­ter to check and if the excep­tion is of type SQL con­nec­tion then write it to a file or email it to webmaster.

  2. You are right about that Trans­ac­tion dead­lock or fail­ure dur­ing estab­lish­ing of a con­nec­tion to a data­base are bet­ter off to be sent to an email or as we use at my work­place a cell phone message.

    Soon i will write proper mail­ing class and will use your advice in there.

    For now we can use cus­tom error pages set via web.config or com­bi­na­tion of lazy and proper method in case there is an excep­tion dur­ing error logging.

    Thanks for feedback

Leave a comment