I will chime in with what @MikeNakis says: the exception type used should be fine-grained enough that you can discern to some reasonable degree what the problem was from looking only at that and possibly the stack trace. If the intent was that one should just throw Exceptions around, then that type would have been made final, sealed or whatever happens to be the term in one's programming language of choice, but that is not the case in any language or framework I know of. In fact, catching Exception is often frowned upon, as it is a cop-out that is very likely to hide real, serious problems.
The exception message should be used to provide details that cannot reasonably be expressed through the type system.
Your second example is actually worse, IMO: you are replacing hard-and-fast types with string literals that are bound to become out of date, misused, changed in ways not expected in some parts of the code, or in any number of other possible ways inaccurate. And in ways that the compiler doesn't have a glimmer of hope of helping you catch, in any language.
Exception messages should not need to be changed. To the extent that they are to be used at all (which should be sparingly, IMO; see previous discussion), they should be meant for programmers (and possibly power users, who can enable display of the messages through some mechanism), not end users. Thus, the exact phrasing becomes less important than the fact that the message clearly describes the conditions that caused the exception but which cannot be adequately expressed through the type system.
Also, unless you really need free-form text messages with no built-in semantics as exception clarifiers (unlikely), you are almost always better off forgetting entirely about building and passing a single string message. Let's say you have a piece of code that requires that a certain variable is within a given range, and must throw an exception otherwise. Instead of (pseudo-C#/.NET; I know you could use string building features like StringBuilder or String.Format, but that's not the point here):
if (x < 3 || x > 10) throw new VeryGeneralException("Got " + x.ToString() + " wanted min 3 max 10");
you might do something like:
public class OutOfRangeException : Exception { public int Value { get; private set; } public int MinValue { get; private set; } public int MaxValue { get; private set; } public OutOfRangeException(int value, int minvalue, int maxvalue) { base("Got " + value.ToString() + " wanted min " + minvalue.ToString() + " max " + maxvalue.ToString()); this.Value = value; this.MinValue = minvalue; this.MaxValue = maxvalue; } } ... then far later ... if (x < 3 || x > 10) throw new OutOfRangeException(value: x, minvalue: 3, maxvalue: 10);
That way:
- by looking at only the exception type name, you know that some value was out of its allowed range
- with the resulting generated message, you know the value of the variable in question and its expected range
- along with the full stack trace, you have a pretty decent idea how you got to that point (which, depending on the architecture and the semantics of the value in question, may or may not help you determine why the value was out of its allowed range)
This way, a single exception, including its stack trace, gives you about as much detail as you can hope to get short of a step-by-step description of how to repeat the error.
MyUniqueExceptionorApplicationExceptionor something that's clearly not the built-in superclass.