Question

Using C++, often I hear that you should avoid throwing exceptions for flow control and you should avoid calling functions in conditions where you know they will throw. For example, if a function throws when you pass it an empty string, why not check for empty before calling that function?

I personally as of late have been letting functions throw even in cases where I know they could based on violation of preconditions. Note that these are runtime cases and are not logic errors. Using the string example, this might be a server response that contains an empty string (and in this case we couldn't do anything meaningful with an empty string).

Personally, I like the idea of all failures unwinding the stack to a central point where errors can be handled uniformly, instead of having code branching and special case logic to handle individual points of failure. This can get really nasty across multiple depths of function calls. There's a lot of simplicity and less error-proneness in writing code to assume success, and let the underlying language/tooling interrupt flow control in exceptional/error cases. In the most controversial scenarios, I even throw in my own if conditions instead of returning simply to get the benefit of a central exit/handling point for all exceptions, even ones thrown from functions.

Is this technically "exceptions for flow control" that is often discouraged? Where do you draw the line? What's good, what's evil? Is it better to have a mixture of "graceful return" logic driven by if conditions as well as exception handling?

Update:

Here's a code example if it helps. Just keep in mind this is greatly simplified, there's usually a lot more failure points and the recovery logic is more sophisticated:

void DoThingWithResponse(std::string const& data)
{
    if (data.empty())
    {
        throw std::runtime_error{"Some exception because data was empty"};
    }

    // Do real thing with data, because we know it's valid here
}

void ResponseCallback(std::string const& data, int http_status_code)
{
    try
    {
         if (http_status_code != 200)
         {
             throw std::runtime_error{"Server indicated failure"};
         }

         DoThingWithResponse(data);
    }
    catch (std::exception const& e)
    {
        std::cerr << "Something bad happened: " << e.what();
        RetryRequest();
    }
}
Was it helpful?

Solution

I think we can all agree that this is bad:

void search(TreeNode node, Object data) throws ResultException 
{
    if (node.data.equals(data))
        throw new ResultException(node); // Found desired node
    else 
    {
        search(node.leftChild, data);
        search(node.rightChild, data);
    }
}

And this is bad:

try 
{
    for (int i = 0; ; i++)
        array[i]++;
} 
catch (ArrayIndexOutOfBoundsException e) {} // Loop completed, carry on.

And this is good:

class SomeClass
{
    SomeClass(string requiredParameter)
    {
        if (requiredParameter.NotSupplied)
           throw new ArgumentException("requiredParameter not supplied.");
    }
}

Why are the first two examples so clearly bad? Because they're violating the language's natural idioms. (or the Principle of Least Surprise, if you prefer)

Why is the third example fine? Because, if you don't supply the required parameter, the constructor cannot succeed. There's no meaningful way to recover from that, other than throwing an exception and letting the caller handle it.

So how do you handle the borderline situations? The same way you make any other decision in software development. You weigh the pros and cons, and go with the approach that most effectively meets your specific requirements.

One of the cons is performance. A thrown exception is going to cost you somewhere in the neighborhood of 100 to 10,000 times the cost of an ordinary non-trivial function call or return statement (depending on the programming language), so you cannot throw one in a tight loop where performance is critical, unless its an unrecoverable error condition and you cannot continue the loop.

Licensed under: CC-BY-SA with attribution
scroll top