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.