How Logging Exceptions in C# Gets You Ahead
Although we programmers would like to think our code is beautiful and flawless, the fact is that we make mistakes. What’s worse is that we make mistakes that we don’t foresee until a user complains that our software isn’t working properly. Luckily, software developers have options to overcome the communication disconnect between users and our code. We can log errors that help us drill down into our source and find the problematic code. To paraphrase the famous Code Complete written by Steve McConnell, there are on average 15-50 bugs per 1,000 lines of code.
When Should You Log Exceptions?
The short and sweet answer is that you should always log exceptions. For many developers, the general rule of thumb is to log exceptions when code could crash. The problem with this approach, however, is that even the simplest code can cause crashes in the application.
Here’s what your users see if you don’t handle an exception in a cloud application:
This isn’t exactly a friendly user experience, and without any type of logging it doesn’t help us any more than the user. We have no idea where the application is failing. When we see the yellow screen of death in a C# cloud application, we can use logging to perform analysis. Logging can’t tell you exactly why the application is failing, but it can give you detailed error reports that you can use to find the flawed code.
Error Handling Exceptions Basics
Think of exceptions as your code just saying, “Something went wrong.” We don’t know what went wrong, but luckily the .NET framework contains a library that narrows it down a bit. The System.SystemException class contains several types of exceptions that you can use to log errors. There is also the general “Exception” error.
Let’s take a look at the list of exceptions that can be thrown in C#.
|System.IndexOutOfRangeException||Handles errors when your code references an array index out of range.|
|System.ArrayTypeMismatchException||Handles errors when your code references the wrong data type in an array.|
|System.NullReferenceException||Handles errors when your code references a null object. This is one of the more common coding bugs.|
|System.DivideByZeroException||Handles errors when you attempt to divide by zero.|
|System.InvalidCastException||Handles errors when you attempt to cast to a variable that cannot be cast to the specified data type.|
|System.OutOfMemoryException||Handles errors when your code runs out of memory.|
|System.StackOverflowException||Handles errors from a stack overflow such as an infinite loop.|
C# handles IO errors using the System.IO.IOException class. Use this Exception class for general file errors.
With the list of exceptions, we can now implement a try-catch to catch errors from our code.
For instance, suppose we accidentally divide by zero. Our code would look like the following:
In the code above, it’s obvious that the division will throw an error. When the code is executed, the catch method is initialized. The error is held in the “e” variable, and we can now do whatever we want with the error. This is where logging ties in.
Logging Your Bugs
Once you have all of your try-catch blocks set up, you need to decide what you want to do when an exception is thrown. Logging is one of the best ways to handle these errors because you have a trace of what happened in any given section of your code and the error that was thrown. Logging can’t fix the logic error, but it tells you exactly where the bug is located in your code. You still might need to communicate with users to ask them what they were doing or what input they sent, but most logs can help you identify bugs before your users ever detect them. If you log the bug and then gracefully send users to another page (such as redirecting to an error page in a web-based application), users are unaware that an exception was ever thrown. Not all bugs are directly exposed or recognizable to the user. For instance, a user might blame the network for a slow-loading application when the root cause is really your code.
You have several logging options. The most common for developers is to log directly to the Event Viewer. The Event Viewer is available to any administrator on the web server. It’s also a native way to log errors, because you don’t need any extra code or libraries.
Logging to Event Viewer has its disadvantages. As a developer, if you don’t have access to the web server as an administrator, you have to ask an administrator to get you Event Viewer logs every time something goes wrong. This can be difficult for developers, so many of them decide to use external logging tools. These tools give them the ability to have copies of errors without remotely logging into the server. Loggly is one such option that gives developers and administrators alike the ability to see logs without ever using RDP. Luckily, Loggly uses Nxlog to automatically collect your event logs, so you get dual advantage as a developer – you can log without depending on external libraries, and you can view those logs without asking an administrator to download them every time you find bugs in the system. In addition to log aggregation, you can use Loggly for analytics and powerful root analysis when your application bugs become difficult to pinpoint.
Your Loggly dashboard would look like the following:
So now that we know we want to log to Event Viewer, we just need to plug in our logging code. First, before you start logging, you need to include the System.Diagnostic library in your using statements. Don’t forget to also add System.SystemException in your using statements as well.
Let’s go back to our buggy “divide by zero” example.
We first need to define the source, the name of the log, and the name of the event.
The sourceApplication variable is the name of your application. This helps you quickly find events related to your application in Event Viewer. The logName variable is given the name “Application.” As you know, there is already a log in Event Viewer named Application. You can use this same one or create a new one. In our example, we’re just using the standard Application log.
Next, we have eventName. We’re going to assume that this exception handling will be done on a login page. The “Login Error” value tells us that the event happened while the user was logging in.
Now, let’s tie in logging with the division exception.
That’s it. That’s all it takes to log to your exceptions. The last parameter in the WriteEntry event is the type of event that you raise. Event Viewer has Info, Warning, and Error events. In this example, we just raised a warning, but an Error type would be necessary if a critical issue blocked the user from logging in.
Some General Guidelines for Exception Handling
Error handling and logging are easy, but the hardest part of the process is determining where you should log. Let’s look at this code.
The code above uses the method DivideNumbers to perform the calculation and return the result to the myNumber variable. Of course, this also creates a DivideByZero Exception. The question is where do you put the try-catch? Wrap the variable assignment or in the function?
If you log further up the stack, you can bubble up errors at the top level for you to investigate specific calls to the method. For instance, suppose three classes call a specific method. If you log within the lower-level method, you only know that the method threw an error, but you don’t know which calling class caused the issue. For this reason, it’s better to log at the highest level, so you know where the error started.
Some Do’s and Don’ts for Exceptions
- Don’t just wrap an entire method with one try-catch. For instance, each C# MVC project has an Index method for each page. Don’t just place a try-catch around all of your code. Place try-catch around specific code.
- Do be descriptive. Your logs should contain specific information that helps you identify the root of an error. Don’t just log “error.” Log “Critical error in logging class” and then log the exception information.
- Don’t have an empty catch statement. In other words, don’t just “do nothing” with the caught exception. You should log the information and then either redirect the user or alert the user that something went wrong.
- Do inherit from the Exception class for custom exceptions. You can define your own exceptions for specific programming scenarios. For instance, you might want to define a specific error for your data layers to catch connection issues and bugs.
- Don’t throw errors that are actually not errors. For instance, if we query a database for a list of users and no users are returned, this isn’t an error. An empty data set is not an error even if logically it shouldn’t happen. Handle this type of scenario using logic workflow.
While developers tend not to think of logging as a hard and fast requirement in application development, it should be a priority during design and implementation. It can reduce the time necessary to perform root cause analysis, and it can help identify issues before your customers do. Exception handling isn’t an option, and you can couple it with logging to ensure you have a better debugging and quality assurance methodology after your code is deployed to production.