1

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();
    }
}
  • Hm. Does the functions contract guarantee that it will throw under the circumstances, or is it left open? If the former, consider what is more costly, considering development, maintainance and efficiency, and whether that's significant enough to matter. If the latter, well there is nothing to talk about. – Deduplicator Jun 4 '18 at 14:21
  • I think the guarantee is mostly open. As far as the language goes, the only strict guarantee you can make is that something will not throw. The alternative is can throw. In addition to that, I can document cases that are guaranteed to throw based on logic/business rules, but that is by no means comprehensive (and usually never is, so I think 99% of the time it's the latter). Sorry if I am misunderstanding your question. – void.pointer Jun 4 '18 at 14:28
  • I'm wondering why you think throwing on a precondition failure constitutes "flow control." If you can't throw there, then where can you throw? – Robert Harvey Jun 4 '18 at 15:06
  • 1
    @RobertHarvey: The point of a "precondition" is that if you violate it, you get undefined behavior. Throwing is well-defined behavior; it's a part of the function's interface. That is, there's a difference between a function which can't handle an empty string and a function that throws on getting an empty string. Just as there is a difference between a function that can't handle an empty string and a function that simply doesn't do anything to an empty string. – Nicol Bolas Jun 4 '18 at 15:57
  • 2
    @RobertHarvey: Defined behavior has costs (in this case, a condition+throw). We don't like paying costs needlessly in C++ land. Preconditions are things that an API assumes to be true. Defined behavior is about things that will be verified to be true. – Nicol Bolas Jun 4 '18 at 16:47
8

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.

  • Well, if we talk Java or C#, you are right, programmer error nearly always should result in exceptions. It's C++ though, so we can easily make it impossible to not supply the required parameter without violating the language (making sure it's sensible is likely too much for the type-system though), in which case all bets are off anyway. Or we would prefer assert() for this type of error most of the time. – Deduplicator Jun 4 '18 at 15:47
  • 3
    Ergo, the "natural idioms" thing I mentioned. I think language idioms are a good way to think of this; if you're not violating the programming language's natural way of doing things, then I think you can safely take a "pro/con" approach. – Robert Harvey Jun 4 '18 at 16:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.