Overview
Most articles on exceptions cover only the ideas and techniques of exception handling. The reason is simple: most of us write end-user software and so throwing an exception makes little sense. However, if you are writing a custom component, the only way to notify the user of your component when something goes wrong is to throw an exception.
In this article, I'm going to give you some ideas as to when throw an exception, why take the effort to define a custom exception class, how to provide more information to the developers that consume your component and, finally, how to unit test your exceptions. I'll try to avoid labels like "good" or "bad," so that you can see what's best for yourself.
Introduction
Throwing an exception is usually done to tell some other part of your program that something's gone wrong. Doing this in the GUI layer rarely makes sense, since you have other means of handling this situation: just show a message, do some logging, try to recover or just quit. Doing it in the business layer is more appropriate: you don't want to show any visual elements from here, and you definitely don't want to close the application, so if you can't recover, at least tell the application that something went wrong. In simple cases, you can do just fine with a general Exception class, but if you want to handle different "wrongs" in a different manner or, for example, log more information about this "wrong," you have to invent a custom exception and put all of the relevant information into that class.
When you are building a custom component or when you work in a team, custom exceptions become a necessity. Let's see it in more detail.
Custom Exceptions
Typically you want to throw custom exceptions in two different situations. The first is when some other component calls your method and supplies an invalid argument. Sometimes throwing an InvalidArgumentException
is fine, but if you want to help the developer who uses your component, you might want to throw a custom exception here. The second situation is when an underlying component throws an exception. We'll discuss these one-by-one.
Invalid Input
Suppose you provide a component that just divides x by y. You want to check that y is not zero and throw an exception if it is.
Public Function Divide(ByVal x As Single, ByVal y As Single) As Single
If y = 0 Then Throw New ArgumentException("y shouldn't be zero")
Return x / y
End Function
If a user of the program encounters this error, she might call support -- the developer that used your component -- and say that she received a weird error stating, "Y shouldn't be zero." The developer then searches through his code and can't see where this exception came from. Now let's rewrite our code
Public Function Divide(ByVal x As Single, ByVal y As Single) As Single
If y = 0 Then Throw New DivisionByZeroException()
Return x / y
End Function
The developer immediately identifies the cause, since the exception message would be something like "DivisionByZero exception wasn't handled." In the calling code, he would be able to catch and handle this specific kind of exception.
Now the example might seem a little unrealistic, but the general idea is that the existing framework exceptions are too general (they should be) and it is better to provide a more descriptive exception that occurs only in particular situations. One might argue that a descriptive exception message is enough, but in most cases a developer wants to handle different kinds of exceptions differently, such as:
Try
Dim z = myComponent.Divide(x, y)
Catch ex As DivisionByZeroException
Catch ex As OverflowException
Catch ex As Exception
End Try
In addition, you might want to provide more data for the caller. You do that by adding some other properties to your exception class, so that the calling code could inspect these properties and make a decision. We'll discuss this later in the article.
Exception Thrown by Another Component
When we call some other code and this code throws an exception, we are tempted to leave it unhandled and let our caller -- presumably the main application -- handle it. I'll show you why this is not a very good idea. Consider the following piece of code:
Function GetConfigValue(ByVal param As String) As Single
Return My.Settings.Item(param)
End Function
We can get three different exceptions here. First, if our config file is missing, we'll have a FileNotFoundException
. Next, if we don't have a setting identified by the parameter argument, a SettingsPropertyNotFoundException
is thrown. Last, if the value can't be converted to a Single
, we'll have an InvalidCastException
. The first and the third are difficult to understand when you are writing the calling code. If I call a GetConfigValue
method, I expect at least an exception related to configuration, not something about files or conversions.
A possible solution would be to create two custom exceptions: MissingConfigFileException
and ConfigValueIsNotSingleException
. It could be convenient to inherit MissingConfigFileException
from FileNotFoundException
, since it already has the FileName
property that could be used by the developer in identifying the problem. In the same way, ConfigValueIsNotSingleException
can be inherited from InvalidCastException
. However, this is not really necessary, since we'll provide the original exception as the InnerException
property. So, our code becomes:
Function GetConfigValue(ByVal param As String) As Single
Try
Return My.Settings.Item(param)
Catch ex As IO.FileNotFoundException
Throw New MissingConfigFileException(ex)
Catch ex As InvalidCastException
Throw New ConfigValueIsNotSingleException(ex)
End Try
End Function
And the definitions for the exceptions are:
Public Class ConfigValueIsNotSingleException
Inherits InvalidCastException
Public Sub New(ByVal ex As Exception)
MyBase.New(ex.Message, ex)
End Sub
End Class
Public Class MissingConfigFileException
Inherits IO.FileNotFoundException
Public Sub New(ByVal ex As IO.FileNotFoundException)
MyBase.New(ex.Message, ex.FileName, ex)
End Sub
End Class
Typically, you would add more properties to your exception class so that the caller has more information about the exception. For example, we could add the ParameterName
and ActualValue
properties to our ConfigValueIsNotSingleException
class. These properties would be ReadOnly
and the corresponding private fields should be set in the constructor.
Public Class ConfigValueIsNotSingleException
Inherits InvalidCastException
Public Sub New(ByVal ex As Exception, _
ByVal paramname As String, ByVal value As Object)
MyBase.New(ex.Message, ex)
Me._parameterName = paramname
Me._value = value
End Sub
Private _value As Object
Public ReadOnly Property ActualValue() As Object
Get
Return _value
End Get
End Property
Private _parameterName As String
Public ReadOnly Property ParameterName() As String
Get
Return _parameterName
End Get
End Property
End Class
...
Function GetConfigValue(ByVal param As String) As Single
Try
Return My.Settings.Item(param)
Catch ex As IO.FileNotFoundException
Throw New MissingConfigFileException(ex)
Catch ex As InvalidCastException
Throw New ConfigValueIsNotSingleException(ex, _
param, My.Settings.Item(param))
End Try
End Function
Let's see how the caller could use the improved exception:
Sub ShowMe()
Try
Dim x = GetConfigValue("StringParameter")
Catch ex As ConfigValueIsNotSingleException
MsgBox(String.Format("The value of {0} is {1}, which is not Single",_
ex.ParameterName, ex.ActualValue))
End Try
End Sub
Clearly, developers can now easily identify what caused the exception. For simplicity, in the following sections we'll omit the two added properties and return to a simpler version of ConfigValueIsNotSingleException
.
Reducing the Number of Exception Classes
While having a custom exception class for each exceptional situation can be very helpful for the developer using your component, having a lot of classes can clutter your namespace and bring confusion. Sometimes it is better to define a single exception class for several situations. We can put some additional information identifying the cause of the exception into a property. For example, rather than having ConfigValueIsNotSingleException
, ConfigValueIsNotIntegerException
, etc., we could have a single class called InvalidTypeInConfigException
for all type conversion errors in the configuration-related code. To identify the exact problem, we could have the Reason
property, which is enumeration-valued. The class definition becomes:
Public Class InvalidTypeInConfigException
Inherits InvalidCastException
Public Sub New(ByVal ex As Exception, ByVal reason As ExceptionReason)
MyBase.New(ex.Message, ex)
Me._reason = reason
End Sub
Private _reason As ExceptionReason
Public ReadOnly Property Reason() As ExceptionReason
Get
Return _reason
End Get
End Property
Public Enum ExceptionReason
ConfigValueIsNotSingle
ConfigValueIsNotInteger
End Enum
End Class
The calling code becomes:
Function GetConfigValue(ByVal param As String) As Single
Try
Return My.Settings.Item(param)
Catch ex As IO.FileNotFoundException
Throw New MissingConfigFileException(ex)
Catch ex As InvalidCastException
Throw New InvalidTypeInConfigException(ex,_
ExceptionReason.ConfigValueIsNotSingle)
End Try
End Function
Exception-related Events
Sometimes you want to notify the caller that an exception is going to be thrown. For example, if the caller has provided invalid data, we could give him a chance to correct the situation. This pattern is used, for example, in the System.Windows.Forms.DataGridView
class. If an error happens, the class checks if there is a handler for this event. If the handler exists, it is invoked. If it does not exist, an exception is thrown. The user is responsible for correcting the error in the handler. The event argument provides, among others, the Exception
property. The handler can inspect this property and, for example, do some logging. The event argument also provides the ThrowException
property, which the handler can set to True
or False
, depending on whether we actually want to throw this exception or handle it in a more peaceful way.
In order to implement this pattern, we have to construct another class for our event argument. Our class should contain the ReadOnly
exception property, the ThrowException
property and some other properties that the caller can modify in order to handle the situation. Let's see an example:
Public Class UnhandledConfigExceptionEventArgs
Inherits EventArgs
Private _exception As InvalidTypeInConfigException
Public ReadOnly Property Exception() As InvalidTypeInConfigException
Get
Return _exception
End Get
End Property
Private _throw As Boolean = True
Public Property ThrowException() As Boolean
Get
Return _throw
End Get
Set(ByVal value As Boolean)
_throw = value
End Set
End Property
Private _value As Object
Public Property ActualValue() As Object
Get
Return _value
End Get
Set(ByVal value As Object)
_value = value
End Set
End Property
Public Sub New(ByVal exception As InvalidTypeInConfigException, _
ByVal value As Object)
Me._exception = exception
Me._value = value
Me._throw = True
End Sub
End Class
Next, the class that contains our GetConfigValue()
method should have an event:
Public Event ConfigException As EventHandler(_
Of UnhandledConfigExceptionEventArgs)
Now, let's see how to invoke this pattern. For simplicity, let's ignore the possibility of having a FileNotFoundException
. Our purpose is to let the consumer of our component handle the situation when the configuration value cannot be converted to Single
and possibly provide an alternative value.
Function GetConfigValue(ByVal param As String) As Single
Dim value = My.Settings.Item(param)
Try
Return CSng(value)
Catch ex As InvalidCastException
Dim ConfigException As New InvalidTypeInConfigException(ex,_
ExceptionReason.ConfigValueIsNotSingle, param, value)
Dim e As New UnhandledConfigExceptionEventArgs(ConfigException, value)
RaiseEvent ConfigException(Me, e)
If e.ThrowException Then
Throw ConfigException
Else
Return e.ActualValue
End If
End Try
End Function
If an exception is thrown, we raise the corresponding event. The caller now has an option to handle the event, examine the Exception
property and perhaps provide a custom value via the ActualValue
property. Let's see it in action:
Sub InvalidTypeInConfigExceptionThrown(ByVal sender As Object, ByVal e As_
UnhandledConfigExceptionEventArgs)
e.ActualValue = 0
e.ThrowException = False
LogExeption(e.Exception)
End Sub
Back to GetConfigValue()
. After the event is raised, we examine the ThrowException
property. Before the event, it had been set to True
, but the handler could have set it to False
. If it has, we return the modified value. Hopefully it's been modified to a Single
value or we'll have another InvalidCastException
. If ThrowException
is still True
, we throw our custom exception, just as we did in the previous section.
This pattern doesn't make much sense if the developer is the one who calls GetConfigValue()
, since the exception can be handled in a straightforward try-catch way. However, if the method is called from within our custom or some third-party component, the developer cannot control the return value of the GetConfigValue()
method unless we provide this event. In a perfect world, we should be throwing an exception each time we have something wrong with our environment, so that our GetConfigValue()
method can't provide the correct value. However, there might be cases where the user of our component does not want the exception to be thrown, but rather wants the normal flow to be continued using a "fake" method result.
So, you can use this pattern only if our method is called from inside our component. Sometimes it makes sense to prevent the exception from occurring and let the rest of the code be executed.
Exception Messages and Localization
The above exceptions return the same message that the underlying exceptions provide. This could be confusing if the calling code forgot to handle this particular exception. After all, you went this far to provide a custom exception. Why not provide a custom message?
You probably don't want to put the message into your code. A more appropriate place is a resource. Another reason to use it is that your message becomes easily customizable.
So, suppose we added two string resources: ConfigValueIsNotSingleMessage
and ConfigValueIsNotIntegerMessage
. The message could be a format string, something like "Value {0} of setting {1} is not a single number." We should override the Message
property:
Public Overrides ReadOnly Property Message() As String
Get
Select Case Reason
Case ExceptionReason.ConfigValueIsNotSingle : Return _
String.Format(My.Resources.ConfigValueIsNotSingleMessage,_
ActualValue, ParameterName)
Case ExceptionReason.ConfigValueIsNotInteger : Return _
String.Format(My.Resources.ConfigValueIsNotIntegerMessage, _
ActualValue, ParameterName)
End Select
Return MyBase.Message
End Get
End Property
Testing
Of course, we'd love to write unit tests for our custom component. While testing the GetConfigValue
method, we should test that the exceptions are thrown when they should be. Testing frameworks usually provide ExpectedExceptionAttribute
for the test methods that test throwing exceptions. However, this way we can only verify that our exception is thrown. We, on the other hand, would like to verify that our exception has correct property values. Let's see the test code:
<Test()> _
Sub GetConfigValueThrowsExceptionWithConfigValueIsNotSingleReason()
Try
Dim x = GetConfigValue("StringParameter")
Assert.Fail("InvalidTypeInConfigException has not been thrown")
Catch ex As InvalidTypeInConfigException
Assert.AreEqual(_
InvalidTypeInConfigException.ExceptionReason.ConfigValueIsNotSingle,_
ex.Reason, "Reason should be ConfigValueIsNotSingle")
End Try
End Sub
We don't use the ExpectedExceptionAttribute
here because we are catching our exception. So, in order to verify that it's been thrown, we use the Assert.Fail
statement. If the exception is being thrown, we never reach this line. If the exception is thrown, but it is an exception of another type, we don't catch it and the test fails. If the exception is of the correct type, we catch it and verify all of the relevant properties. In our case, this is the Reason
property.
Serializing Exceptions
The base Exception class is marked as Serializable. It means that it could be passed to another AppDomain, or even another application. Although it rarely happens, you should probably be prepared for this scenario. By default, only the inherited members are serialized. Whenever your exception crosses the AppDomain boundary, it loses all the custom fields you added. Preventing this is simple — you override the GetObjectData
method for serialization and add a certain protected constructor for deserialization. However, it is very easy to forget these things.
Conclusion
Exceptions don't get much attention from the community, especially from the big guys. One obvious reason is that perhaps we still expect our code to be perfect. An exception is something so irregular and annoying that, while our "right" code should be well-structured and have all other nice qualities, we allow our exceptions to be pretty ugly. Another reason, maybe more subconscious, is a sort of primitive superstition: don't talk about exceptions or they'll come and get ya.
I strongly believe that for every programming pattern out there -- be it GoF, Microsoft or your own -- there should be a corresponding pattern of exception handling and/or raising. At least the exceptions should be made an essential part of the pattern. After all, exceptions are classes, so many existing patterns could be applied to them. Perhaps someday we'll hear about Exception Factories, Exception Strategies and Exception Observers. However, since exceptions are not simple objects, depending on your attitude towards them, they can bring chaos into your elegant design or provide controlled execution flow and information exchange between application layers. Exceptions are there, like it or not, and you should turn them into your allies or they'll quickly become your enemies.
History
- 9 July, 2007 -- Original version posted
- 26 July, 2007 -- Article edited and moved to the main CodeProject.com article base
- 22 September, 2007 -- Added the section on serialization