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();
}
}