Error Handling with Exceptions: When to Throw

By | November 1, 2010

This article is part of a longer running series on Error Handling with Exceptions.

It can be a challenge to know when to throw exceptions and, unfortunately, the common guidance to throw in exceptional circumstances doesn’t do much to clarify the confusion. In this article, I will put forward postulates of when to throw exceptions and then explain each of those in more detail.

Note: In this article I will use the term function and method interchangeably. With regards to discussions of exceptions, precision associated with the differences between the two terms is not germane to the discussion.

When to Throw Exceptions

  1. Exceptions should be thrown when the intended purpose of a function cannot be completed
  2. When performance characteristics associated with exceptions for a particular function are prohibitive to the function’s use, a second, opt-in “nothrow” function should be created
  3. “nothrow” functions cannot be created if there is more than 1 potential cause of failure within the function
  4. The vast majority of mutable member functions should not return a value
  5. All immutable member functions should return a value

Exceptions should be thrown when the intended purpose of a function cannot be completed

A function should do one thing, and that “one thing” should be captured in the return value (or an object state change if writing a mutable member function). A function’s failure to accomplish its intended purpose implies that a return value cannot be created and should result in an exception. We run into trouble when we attempt to:

Overload a return value with special meaning

// rval < 0 on failure
int ReturnPositiveNumber();

In this example, the return value represents a domain that is intentionally larger than the domain of a valid return value. While this does work, our selection of a return value is artificial, is not able to express valid constraints, and may restrict the domain of valid values (for example, what if std::numeric_limits::max() + 1 is a valid return value?). Furthermore, validation logic must be implemented by the caller. In the example above, validation is fairly intuitive – the function succeeded if the return value is greater than 0. However, what is the validation logic in this code below?

// Is nullptr valid?
MyObject * GetObject(); 

Again, the validation for this function may be fairly intuitive. However, any ambiguity introduced into code imparts some amount of burden on to the caller, and this burden is likely to lead to bugs when multiple developers are working within a code base.

Eschewing Return Values by Returning Error Codes Some systems require that functions return error codes, where values within the return type’s domain are well known. For example, COM requires that methods return HRESULTs to indicate success or failure. This solution does do a great deal to eliminate ambiguity associated with overloaded return values, but users pay a high price as a result.

HRESULT DoSomethingWithCOM(IUnknown *pUnknown);

When consuming the code above, HRESULTs contain rules that differentiate between values that indicate success and values that indicate failure. However, rigid solutions such as this prevent the return of actual results, meaning that the caller must create temporary variables to store pass as arguments when calling the function (note that it is possible to return successful values that can be overloaded to indicate a function’s result (S_FALSE), but such practices are generally discouraged).

Most troublesome, solutions such as this tend to be viral – it can be very difficult to communicate error codes to callers without returning the error code itself. This can result in a full call stack with knowledge of HRESULTs but no direct dependency on COM whatsoever. Leaky abstractions such as this immediately decrease the likelyhood of maintaining a layered architecture.

When performance characteristics associated with exceptions for a particular function are prohibitive to the function’s use, a second, opt-in “nothrow” function should be created

Sometimes, it makes sense to look before you leap. For example, it probably doesn’t make sense to throw exceptions in the following contrived code example:

for(int iCtr = 0; iCtr < 100000000; ++iCtr)
{
    int  iValue(Parse("This is not an int"));

    // ...
}

It may be advantageous to create a function similar to the first that does not throw an exception when invalid input is encountered. Fortunately, the new function does not violate the first postulate as its new name will reflect the intended return value’s purpose.

int Parse(char const *pszString);
bool TryParse(char const *pszString, int &value);

While helpful, this technique should be used with care, and these guidelines should be followed:

  • Psychologically, the function that requires less discipline on the part of the caller (in this case, Parse) should be the more discoverable of the two variations. A user should knowingly opt-in to use a function that requires additional actions on their part (while this is actually accomplished with the name of the function itself, is can be valuable to make the more advanced function require slightly more effort to type). Note that leading a user towards the first function might have a negative impact on performance, but it is better to be correct that it is to be performant.

  • The default function should always be implemented in terms of the advanced function (DRY: Don’t Repeat Yourself) as in the code below.

int Parse(char const *pszString)
{
    int value(0);

    if(TryParse(pszString, value) == false)
        throw std::invalid_argument("pszString");

    return(value);
}

bool TryParse(char const *pszString, int &value)
{
    // Parse, return false if invalid
}

“nothrow” functions cannot be created if there is more than 1 potential cause of failure within the function

It is not valid to create a “nothrow” option of a function if there are multiple points of potential failure within a function. It is important for debug-ability to know what error happened, not simply that an error happened. For example, the following function is not valid as there may be multiple reasons why the file could not be opened (file does not exist, file in use, permissions, etc).

bool TryOpenFile(char const *pszFilename, 
                 HandleToFile &file); // BAD!

Again, it is better to be correct than it is to be performant.

The vast majority of mutable member functions should not return a value

By definition, a mutable member function is intended to modify its internal state (generally through the modification of member variables). As such, the purpose of the function is to modify the state of the associated instance and not to return a value consumed by an external entity. While not always true, a mutable member function that returns a value should be subjected to various Code Smell investigations to see if such usage is appropriate.

All immutable member functions should return a value

By definition, an immutable member function should always return a value, as the function should not be modifying its internal state as a result of the function’s execution. Therefore, anything that is done by the function should be captured within the return value.


Leave Your Comment

Your email will not be published or shared. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Please wrap all source codes with [code][/code] tags. Powered by