Defensive programming (ie. checking object for null before having an action on it) can be a great tool but do we always can prevent all the errors especially when it comes to user inputs or web servers that not always reliable. When application is up and running there are not many ways we can find out that some user had an error unless the error is global so application goes down. Of course if we have an access to Windows Event Viewer on the server we can check it daily but in my honest opinion Event Viewer is quite hard to use. Instead of that we can write our own ErrorLogger. So each time user or server have a bug we will have a nice report on that. For error tracking we will use .NET built in Exception class and Try and Catch block.
part I — setting up database
Create a database with name ErrorLog inside of it create a table in database called Errors with following fields errorID, userID, userIP, errorPage, errorName, errorStack, errorDate 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 tracking parameter ie. user Guid), otherwise you can save zero value. Ability to know what user had an error can help you to debug the application. userIP field useful to know whether it is the same user accessed resource with error in case there was no userID provided. errorPage is useful because sometimes there is no indication in Exception’s stack where an error appeared so saving it could be a good idea also in case you have one error logging system for multiple websites this way you will save time understanding where it was or to review an error log at any website. errorName is a short name of the exception that mostly not descriptive enough to debug but useful for short indication or error category needs. errorStack field will be used to store everything from Exception.ToString(). errorDate obviously for sorting and timing issues and isRead is useful in cases when you don’t delete errors but just hide them from viewing.
Next lets write a stored procedure “LogError” which will insert an error to database, 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 capture an error as it appears from global.asax Application_Error() method which i will show you in a few but it is important to understand that the “lazy way” won’t give you much as flexibility as “proper” one simply because you will just log some of errors without the ability to inform user of what really happened. Catching 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 conditional logic so for x error application will redirect z page and y error will redirect somewhere else and show user friend message (ie. if not logged in user tries to get to page that requires userID stored in session or cookie you would like him to be redirected to login page, to show him user friendly message and then to log an error which could be caused by wrong account confirmation 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 explanation. First we get an Exception from Server.GetLastError() object then we connect to database, with “LogError” stored procedure
*** “Lazy method” suitable for those who already have an online application when architectural changes in its design will create needless bugs. Even though “proper method” has much wider flexibility than just logging errors, so next time you start another project consider using the proper one but to use it on live application 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 situation when your application database in one place and ErrorLog on another, by having try and catch block in there we protect user from being thrown from application when ErrorLog database will fail. So we won’t track an error but user will continue to browse our application.
The “proper method”
Lets create a class inside our App_Code folder caller Errors. Add a method in it caller logError. This method it wrapped by try and catch block so we will avoid Exception in case we could not be able to establish connection to Errors database or any other connection error like transaction deadlock. Thanks to Shuaib Rameh for pointing 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 similar to one we added to Application_Error handler inside of global.asax with with difference we pass Exception as parameter and use its values to track an error. Now lets see how to use this method and what its flexibility 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 sample method in which we check whether user’s email exists in our database ie. for password recovery needs or registration. Connection is wrapped up with try-catch block and on Exception we track it down with a method we create inside of Errors class. We can redirect or do what ever we want but error will be logged. Connection here is made with using block for more information about it read HOWTO: Properly connect to MSSQL database .
Tags: ASP.NET, exceptions
Hi,
Great article… but what happens if exception is type of SQL connection?
Your logError method throws an new exception. No?
I think it would be better to check and if the exception is of type SQL connection then write it to a file or email it to webmaster.
You are right about that Transaction deadlock or failure during establishing of a connection to a database are better off to be sent to an email or as we use at my workplace a cell phone message.
Soon i will write proper mailing class and will use your advice in there.
For now we can use custom error pages set via web.config or combination of lazy and proper method in case there is an exception during error logging.
Thanks for feedback