Wednesday, May 8, 2013

'Exception' does not denote 'Infrequent Occurrence”



NOTE: this will generally pertain to C# but applies to any programming language which uses similar exception handling (for example Java). This article covers a few misconceptions about exceptions and is not intended as a complete reference. In depth study of this important programming aspect is highly recommended. 

Overview

The exceptions a method throws are as important as its parameters or return value. They are part of your contract with the user, even if you are not required to declare them as part of your method definitions (as you are in Java). Too often methods are completely written before any thought is given to the exceptions it should throw. This unfortunate behavior leads to a less than ideal interface for the user.

Many developers believe, and in many cases are taught, exceptions exist to handle rare or infrequent conditions encountered during execution. The truth is as developers we have no control over the circumstances our code is used in beyond the compiler enforcing the existence of our parameters. As such we can never say what the frequency of any use case may be. Thus we can never say if any given case is rare or common.

Usage

The correct use of exceptions is to convey our inability to keep our contract with the user as defined by our method signature. Let us consider the following method signature from the Int32 type:

public static int Parse(string s)

This method takes a string representation of an integer and returns its integer representation. Viewing the documentation reveals it throws the following exceptions:

ArgumentNullException – thrown when s is null.
FormatException – thrown when s cannot be converted to an int.
OverflowException – thrown when the representation of s is beyond the capacity of an Int32 to represent.

Each of these exceptions represents a condition which prevents the method from honoring its contract, returning an integer representation of the string parameter, to the user. There is a different exception for each semantically meaningful mode of failure. That is to say each exception represents a different reason the conversion failed giving the caller the ability to make a meaningful decision about how to proceed. The OverflowException can represent failure due to either a value above or below what can be represented by an Int32. As the user would not take different actions based on whether the value is too big or too small we have just one exception to represent both instead of an OverflowHighException and an OverflowLowException.

Example

Using these ideas let us design our own method. Let us suppose we have a type which represents a temperature probe. We might start by writing a class method with the signature:

public float GetTemperature()

[Note: normally we would use a property here but for demonstration purposes we will make this a method. Also, interestingly enough, the CLR doesn't know about properties so C# converts them into methods during compilation]

What exceptions should this method throw? Anything which violates our contract with the user should be represented by an exception. In this case any condition which would prevent us from returning a temperature will need an exception. We could start with the following:

TemperatureProbeNotAvailableException – thrown if our connection to the probe is lost.
TemperatureOffScaleException – thrown if the probe provides a value beyond what its specification states it can accurately measure. 

Previously when considering the Parse method we said there was no need to have multiple exceptions to represent the high and low possibilities for the OverflowException. Here we also have a single exception to represent an out of range condition. Considering further is there any action our users might wish to take based on whether the temperature probe is off scale high vs low? In this case there is.

If the probe were attached to a heating or cooling system knowing which direction the temperature is off scale would be very meaningful information. For a more awesome example let us say this was a very high temperature probe in the exhaust of a gas turbine engine. Many such probes do not operate correctly until they reach a minimum temperature well above normal ambient. An off scale low condition might be normal while the engine was offline or idle but an off scale high might indicate the engine was running beyond specification and should be throttled down.

Since the direction of the off scale condition is potentially meaningful to the user we should provide exceptions for these conditions.

public class TemperatureOffScaleHighException : TemperatureOffScaleException
public class TemperatureOffScaleLowException : TemperatureOffScaleException

Though the off scale direction of the temperature probe could be useful to the user this might not always be the case. By extending the high and low exceptions from the TemperatureOffScaleException we give the user a choice  If they do not care about the direction of the off scale condition they can choose to catch the base TemperatureOffScaleException. Otherwise if they have code to handle the high and low conditions differently they may catch these derived exceptions in separate catch blocks and deal with them as they see fit.

Aren't Exceptions Time Expensive?

Depending on the environment the throwing of exceptions can be time expensive, especially if they repeatedly do so in tight loops. The previously mentioned Int32 type addresses this with the TryParse method. 

public static bool TryParse(string s, out int result)

This method does not throw a FormatException. Instead if will return a boolean indicating if the conversion was successful and if so it will provide the converted value via the result out parameter. By using a boolean instead of an exception this call avoids the cost associated with handling an exception.

Does this method keep its contract with its user? Yes it does.
Whereas the Parse method implies by its signature it will return a converted value TryParse implies it will attempt a conversion. This distinction in naming is important in creating an API your users will be able to easily understand.
A simple look through the FCL will demonstrate the occurrence of TryX is very limited. While the TryX methods seem to match the X method in capacity and excel it in speed it fails in code simplicity. The code necessary to support the TryX method is more cumbersome than the a simple X method.

Conclusion

There are various opinions about the role of exceptions and how they are used. What I would like to convey here is exceptions are an important yet overlooked aspect of development in environments which support them. A more in depth study than what is presented here is recommended though simply considering them at the outset of development instead of as a postscript will help in API design. 








No comments:

Post a Comment