When I mentor developers, exception handling is a common
topic in the Q&A-sessions. Developers with a Visual Basic 6.0 background
remember “on error”-phrases, while Java-developers are marked by the troubles of
checked exceptions. There are two common exception handling strategies used by
many novice .NET-developers; either they catch System.Exception everywhere or
they completely ignore exceptions.
Luckily, people are asking questions and they are willing to
learn more about what to do with exceptions. Questions like “Should I wrap
every code-block with a try/catch?”, “What should I do when an exception
occurs?” and “How can I log an exception?” are all common.
Catching exceptions
The “on error resume next”-construct in old-school Basic was
a really bad idea, so there is no need to recreate the feature using VB.NET or
C#. Nobody really does this intentionally, but in a sense, this is what the
code in Example
1 does.
try
{
this.OperationWillFail();
}
catch {}
Example 1 – “on error resume next” in C#
The example above is an obvious source of unexpected behavior, and
experienced developers seldom write this kind of code. However; some patterns
used by intermediate developers are related to the previous example. For
instance, it is not uncommon to have a method return a Boolean indicating
success or failure, and returning false if an exception occurs within the
method. In some places, such as the Int32.TryParse method in .NET 2.0, this
behavior is appropriate because of the semantics of the method signature;
TryParse has an out parameter. As a rule-of-thumb you should avoid out
parameters, and hence TryParse is an exception to general exception handling
practices. There are multiple reasons why replacing exceptions with success-indicators
is a bad practice, but the most important one is that you remove valuable
information about the cause of the exception making the application difficult
to debug.
Instead of being embarrassed about exceptions occurring in
your code, let the exceptions bubble up through the call-hierarchy to a place
where you have enough contextual information to handle the exception. Consider
the following scenario; you have written a piece of code to import a CSV-file
from a legacy system. The legacy system is unstable, so sometimes the data has
to be retrieved from a backup system. Depending on the availability of the
legacy system, the data the files are stored in different directories. You have
written code similar to Example
2. This code works because you know that if the “LegacySystem”
directory doesn’t exist, you’ll have to retrieve the data from the backup
system. This code would fail miserably if the System.IO team at Microsoft pretended
that the “LegacySystem” directory exists when it doesn’t.
try
{
File.OpenText(@"C:\LegacySystem\Data.csv");
}
catch (System.IO.DirectoryNotFoundException)
{
File.OpenText(@"C:\BackUpSystem\Data.csv");
}
Example 2 – Using contextual information to
handle exceptions.
Exceptions and
Performance
There are two exception related performance issues in the
previous section; with a method signature akin to the TryParse method it is
alright to attempt something as long as you catch any exception that can occur
and in Example
2 it is alright to use exceptions for control-flow.
Observing it from the outside, the .NET 2.0 Int32.TryParse method would be implemented
as in Example
3, but if you take a peek under the covers, you’ll
discover that this the method guards itself from FormatExceptions being thrown
whatsoever.
public bool TryParse(string s, out int i)
{
try
{
int i=int.Parse(s);
return true;
}
catch (FormatException)
{
return false;
}
}
Example 3 – A naïve TryParse implementation
This is essential, because using exceptions for control-flow
has huge performance penalties. Avoiding flow-control through exceptions has
numerous benefits, apart from increased performance; you’re code turns out to
be more readable. The code in Example
4 does the same as Example
2, only faster and the behavior comes across almost as
if it was written in plain English.
if (Directory.Exists(@"C:\ LegacySystem \"))
{
File.OpenText(@"C:\ LegacySystem \Data.csv");
}
else
{
File.OpenText(@"C:\ BackUpSystem \Data.csv");
}
Example 4 – Being conscious about control-flow
Throwing Exceptions
Most of the time, you’ll be playing the role of the catcher,
but sometimes you’ll have to be the one throwing exceptions. You should use the
advice in the previous sections to eliminate the need for an exception to
ensure that your method succeeds performing it’s propose and can return safely
to the caller. However, if your method fails always throw an exception and
never resort to returning error codes. Because of the severe nature of an
exception, an exception will get noticed.
Even though it isn’t; the System.Exception class should be regarded as an
abstract class, therefore you should never throw this exception. Instead you
should try to find a suitable exception from the .NET framework’s vast set of
predefined standardized exceptions. These exceptions are well-known to all
developers, so when such an exception is thrown, developers will have the
necessary knowledge about the circumstances that caused the exception to
resolve the problem if they the contextual information needed.
For instance, you should always throw an ArgumentException,
or an exception derived from this class, if an invalid parameter is passed to
your method.
If you except users of your class library to take
programmatic actions based on exceptions, there are two scenarios where you
should define new exceptions; if none of the standard exceptions in the
framework are applicable or if you need to add additional details to an
existing exception. If you have to define a new exception you should derive
your exception from System.ApplicationException. The Design Guidelines for
Class Library Developers also mentions System.SystemException where System.ApplicationException
is discussed, this class serves as a base class for the exceptions in the
System namespace and unless you’re on the Microsoft BCL team, you won’t be
adding new classes to that namespace. If you’re defining multiple exceptions in
a single namespace, you should consider following the same pattern as Microsoft
does with System.SystemException; that is define a base exception for the
entire namespace and derive from this exception to group the exceptions within
the namespace.
If you need to add additional information to an existing
exception, you should derive from the exception in question in lieu of
embedding the information in the message.
For instance; the .NET framework’s System.DuplicateWaitObjectException
is derived from System.ArgumentException. This makes it a breeze to detect that
an object appears more than once within an array of synchronization objects. If
the BCL-team had stuck to a vanilla System.ArgumentException, you would have to
parse the exception message to detect this. This would be impractical, result
in verbose and redundant code and it wouldn’t have optimal performance.
Back to Catching
Exceptions…
As we learned earlier, you shouldn’t catch exceptions you
can’t handle. This doesn’t imply that it is OK to ignore exceptions and hope
that they can be handled higher in the call-stack. You should catch every
exception you can handle, but don’t get eager and try to handle every possible
exception a method might throw as this will make your code brittle to change
and virtually impossible to test. Even though you shouldn’t throw a pure
System.Exception, you can catch it. This is a much better approach than trying
to catch tons of different exceptions, just remember that you should let
exceptions you can’t handle pass.
Re-throwing
Exceptions
You should avoid re-throwing exceptions unless you’re
exchanging the exception with a new exception or adding additional information
to an exception. When you exchange an exception you should chain the original
exception to the new exception as an inner exception as shown in Example 5. Exchanging exceptions for other exceptions is a
useful technique for making the cause of exceptions clearer to the users of
your library at public boundaries.
try
{
// Open the file...
}
catch (Exception e)
{
throw new IOException("Could not open file.",e);
}
Example 5 – Chaining exceptions
If you need to just re-throw an exception, you should only
use a throw statement without an argument as this compiles to the IL
instruction rethrow instruction. This instruction is only permitted within
catch-block and it is used to re-throw the current exception without altering
the stack trace. If you use throw with the caught exception as an argument, the
stack trace is rooted to your method as opposed to the exception’s real origin.
Some Final Words on
Finally
While catching and re-throwing exceptions is far better than
just catching exceptions from a debugging point-of-view, you should try to use
the try/finally pattern as often as possible. The code in a finally block is
always executed, hence the finally block is a perfect place to cleanup when an
exception occurs while still allowing the exception to be raised. If you make a
habit of using this pattern, you won’t accidentally forget to re-throw an
exception in the catch block either.
Conclusion
Exception handling is a vast topic and I’ve merely scratched
the surface here. If you wish to dive deeper into best-practices the Error
Raising and Handling Guidelines from Design Guidelines for Class Library
Developers is a good place to start. One important lesson to learn is that you
shouldn’t “eat” exceptions by catching them without taking any action. Another
is that exceptions should truly be exceptional; therefore you should never use
exceptions for flow-control or any other common tasks.