Error Handling in Large NET Projects - Best Practices
Error Handling in Large NET Projects - Best Practices
Abstract: Effective error and exception handling in any kind of an application plays an
important role in providing a pleasant experience to the user, when unexpected failures occur.
This article talks about some effective error handling strategies that you can use in your
projects.
As our applications grow, we want to adopt a manageable strategy for handling errors in order
to keep the user’s experience consistent and more importantly, to provide us with means to
troubleshoot and fix issues that occur.
This article is published from the DNC Magazine for Developers and Architects. Download or
Subscribe to this Free magazine [PDF] to access all previous, current and upcoming editions.
1/10
try
{
// code which can throw exceptions
}
catch
{
// code executed only if exception was thrown
}
finally
{
// code executed whether an exception was thrown or not
}
Whenever an exception is thrown inside the try block, the execution continues in the catch
block. The finally block executes after the try block has successfully completed. It also
executes when exiting the catch block, either successfully or with an exception.
In the catch block, we usually need information about the exception we are handling. To grab
it, we use the following syntax:
catch (Exception e)
{
// code can access exception details in variable e
}
The type used (Exception in our case), specifies which exceptions will be caught by the catch
block (all in our case, as Exception is the base type of all exceptions).
Any exceptions that are not of the given type or its descendants, will fall through.
We can even add multiple catch blocks to a single try block. In this case, the exception will be
caught by the first catch block with matching exception type:
catch (FileNotFoundException e)
{
// code will only handle FileNotFoundException
}
catch (Exception e)
{
// code will handle all the other exceptions
}
This allows us to handle different types of exceptions in different ways. We can recover from
expected exceptions in a very specific way, for example:
If a user selected a non-existing or invalid file, we can allow him to select a different file or
cancel the action.
If a network operation timed out, we can retry it or invite the user to check his network
connectivity.
2/10
For remaining unexpected exceptions, e.g. a NullReferenceExceptions caused by a bug in the
code, we can show the user a generic error message, giving him an option to report the error,
or log the error automatically without user intervention.
catch (Exception e)
{ }
Doing this is bad for both the user and the developer.
The user might incorrectly assume that an action succeeded, where infact it silently failed or
did not complete; whereas the developer will not get any information about the exception,
unaware that he might need to fix something in the application.
Hiding errors silently is only appropriate in very specific scenarios, for example to catch
exceptions thrown when attempting to log an error in the handler, for unexpected exceptions.
Even if we attempted to log this new error or retried logging the original error, there is a high
probability that it would still fail.
void MyFunction()
{
try
{
// actual function body
}
catch
{
// exception handling code
}
}
This is often appropriate for exceptions which you can handle programmatically, without any
user interaction. This is in cases when your application can fully recover from the exception
and still successfully complete the requested operation, and therefore the user does not need
to know that the exception occurred at all.
However, if the requested action failed because of the exception, the user needs to know
about it.
3/10
In a desktop application, it would technically be possible to show an error dialog from the same
method where the exception originally occurred, but in the worst case, this could result in a
cascade of error dialogs, because the subsequently called functions might also fail – e.g. since
they will not have the required data available:
GetPersonEntity will catch the exception and show an error dialog to the user.
Because of the previous failure, ApplyChanges will fail to update the PersonEntity with
the value of null, as returned by the first function. According to the policy of handling the
exception where it happens, it will show a second error dialog to the user.
Similarly, Save will also fail because of PersonEntity having null value, and will show a
third error dialog in a row.
To avoid such situations, you should only handle unrecoverable exceptions and show error
dialogs in functions directly invoked by a user action.
In contrast, UpdatePersonEntity and any other functions called by it should not catch any
exceptions that they cannot handle properly. The exception will then bubble up to the event
handler, which will show only one error dialog to the user.
In an MVC application, for example, the only functions directly invoked by the user are action
methods in controllers. In response to unhandled exceptions, these methods can redirect the
user to an error page instead of the regular one.
4/10
To make the process of catching unhandled exceptions in entry functions simpler, .NET
framework provides means for global exception handling. Although the details depend on the
type of the application (desktop, web), the global exception handler is always called after the
exception bubbles up from the outermost function as the last chance, to prevent the
application from crashing.
From the user standpoint, it should display a friendly error dialog or error page with instructions
on how to proceed, e.g. retry the action, restart the application, contact support, etc. For the
developer, it is even more important to log the exception details for further analysis:
File.AppendAllText(logPath, exception.ToString());
Calling ToString() on an exception will return all its details, including the description and call
stack in a text format. This is the minimum amount of information you want to log.
To make later analysis easier, you can include additional information, such as current time and
any other useful information.
Where should the files be located? Most applications do not have administrative
privileges, therefore they cannot write to the installation directory.
How to organize the log files? The application can log everything to a single log file, or
use multiple log files based on date, origin of the error or some other criteria.
How large may the log files grow? Your application log files should not occupy too much
disk space. You should delete old log files based on their age or total log file size.
Will the application write errors from multiple threads? Only one thread can access a file
at a time. Multiple threads will need to synchronize access to the file or use separate
files.
There are also alternatives to log files that might be more suitable in certain scenarios:
Windows Event Log was designed for logging errors and other information from
applications. It solves all of the above challenges, but requires dedicated tooling for
accessing the log entries.
If your application already uses a database, it could also write error logs to the database.
If the error is caused by the database that’s not accessible, the application will not be
able to log that error to the database though.
You might not want to decide on the above choices in advance, but rather configure the
behavior during the installation process.
Supporting all of that is not a trivial job. Fortunately, this is a common requirement for many
applications. Several dedicated libraries support all of the above and much more.
Although, there are of course differences between the two, the main concepts are quite
similar.
In NLog, for example, you will typically create a static logger instance in every class that needs
to write anything to the log:
To write to the log, you will simply call a method of that class:
logger.Error(exception.ToString());
As you have probably noticed, you have not yet specified where you want to log the errors.
Instead of hardcoding this information in the application, you will put it in the NLog.config
configuration file:
6/10
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns=http://www.nlog-project.org/schemas/NLog.xsd
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets>
<target name="logfile" xsi:type="File" fileName="errors.log"
layout="${date:format=yyyyMMddHHmmss} ${message}" />
</targets>
<rules>
<logger name="*" minlevel="Error" writeTo="logfile" />
</rules>
</nlog>
The above configuration specifies that all errors will be logged to the errors.log file and
accompanied with a timestamp.
However, you can easily specify a different type of target instead of a file or even add additional
targets to log the errors to multiple locations.
With the layout attribute, you define which additional metadata you want to log along with the
message. Different targets have different additional attributes to further control the logging
process, e.g. archiving policy for log files and naming based on current date or other
properties.
Using the rules section, you can even configure logging of different errors to different targets,
based on the name of the logger that was used to emit the error.
Instead of only logging errors, you can log other information at different levels (warning,
information, trace…) and use the minlevel attribute to specify which levels of log messages to
write to the log and which to ignore.
You can log less information most of the time and selectively log more when troubleshooting a
specific issue.
Editorial Note: If you are doing error handling in ASP.NET applications, check out Elmah.
Based on the logged information, we want to be able to detect any problems with the
application and act on them, i.e. fix them to ensure that the application runs without errors in
the future.
Receive notifications when errors happen, so that we do not need to check all the log
7/10
files manually. If errors are exceptional events, there is a high probability that we will not
check the logs daily and will therefore not notice the errors as soon as we could.
Aggregate similar errors in groups, so that we do not need to check each one of them
individually. This becomes useful when a specific error starts occurring frequently and
we have not fixed it yet since it can prevent us from missing a different error among all
the similar ones.
As always, there are ready-made solutions for these problems available so that we do not
need to develop them ourselves.
The product is tightly integrated into Visual Studio. There is a wizard available to add
Application Insights telemetry to an existing project or to a newly created one. It will install the
required NuGet package, create an accompanying resource in Azure and link the project to it
with configuration entries.
This is enough for the application to send exception data to Application Insights. By adding
custom code, more data can be sent to it. There is even an NLog custom target available for
writing the logs to Application Insights.
Before you can start using it, you first need to install the server application on your own
machine.
There is no wizard available for configuring OneTrueError in your application, but the process
is simple:
- Install the NuGet package for your project type, e.g. OneTrueError.Client.AspNet for
ASP.NET applications.
- Initialize automatic exception logging at application startup, e.g. in ASP.NET applications, add
the following code to your Application_Start method (replace the server URL, app key and
shared secret with data from your server, of course):
As with Application Insights, you can send more telemetry information to OneTrueError with
custom logging code. You can inspect the logged information using the web application on
your server.
Both Application Insights and OneTrueError, as well as other competitive products, solve
another problem with log files: large modern applications which consist of multiple services
(application server, web server, etc.), and are installed on multiple machines for reliability and
load balancing.
Each service on each machine creates its own log and to get the full picture, data from all
these logs needs to be consolidated in a single place.
By logging to a centralized server, all logs will automatically be collected there with information
about its origin.
Of course, we would want to get similar error information for those applications as well, but
there are some important differences in comparison to server and desktop applications that
need to be considered:
There is no way to easily retrieve data stored on mobile devices, not even in a corporate
environment. The application needs to send the logs to a centralized location itself.
You cannot count on mobile devices being always connected; therefore, the application
9/10
cannot reliably report errors to a server as they happen. It needs to also store them
locally and send them later when connectivity is restored.
Just like Application Insights and OneTrueError provide a complete solution for server
application, there are dedicated products available for mobile applications.
Typically, they are not limited to error or crash reporting but also include support for usage
metrics and (beta) application distribution.
Conclusion:
Error handling is a broad topic, but also a very important part of application development. The
actual project requirements will depend on various factors such as application size,
deployment model, available budget, planned lifetime and others.
Nevertheless, it is a good idea to devise an error handling strategy for a project sooner rather
than later. It will be easier to implement and will provide results sooner.
10/10