An exception should be descriptive. This is a combination of three things:
- The identity (type) of the exception.
- The message contained in the exception.
- Other metadata contained in the exception such as a chained exception or error code.
Please note that different languages and runtimes have slightly different exception semantics, but these three ideas are fairly universal. At the very least, an exception has a type and usually has a message, but additional data is not always relevant. Also note that some languages such as C++ do not have a "root" exception type: while C++ does have std::exception
, one may throw anything including primitives.
Picking on Java, there are four core types that form the backbone of the exception hierarchy: Throwable
, Error
, Exception
, and RuntimeException
. While none are abstract, none are intended to be thrown directly: one should use a more specific type. That being said, the fact that none are abstract means it is possible to instantiate and throw them directly.
Back to your original point: generally, one should throw specific exception subtypes because it makes it easier for specific exceptions to bubble up the call stack to be caught at the appropriate level. Consider the following Java example:
public void doSomething() throws Exception {
doOneThing(); // Throws Exception
doAnotherThing(); // Throws IOException
}
If doSomething()
is not an appropriate place to handle the exception, the method calling it must know what exceptions to catch. The order of the catch
clauses is also important: they must go from specific to general. Catching Exception
first will swallow all exceptions, including IOException
, in the same block. This becomes a little tougher to figure out what to catch and what to do with the exception once caught. Consider this code instead:
public void doSomething() throws SQLException, IOException {
doOneThing(); // Throws SQLException
doAnotherThing(); // Throws IOException
}
It is now far less ambiguous what the possible errors are, and there is no coupling in terms of the order in which they are caught. Instead of this:
try {
doSomething();
}
// Order matters!
catch (IOException e) {
// Obviously some sort of file or socket I/O error.
}
catch (Exception e) {
// What are we dealing with here?
}
We have this:
try {
doSomething();
}
catch (IOException e) {
// Obviously some sort of file or socket I/O error.
}
catch (SQLException e) {
// Now we can get the error code and do something meaningful.
}
You can also allow some exceptions to pass through up the call stack, and others to be handled in that try/catch block. For example, the first example would make it difficult to allow SQLException to pass up the call stack while handling IOException, because it is not declared properly. While possible, it would require casting, type checking, and would be brittle and unclean code.
The second example is more clear in terms of intent and allows more useful operations to be performed using the exception object.
Note that the above examples make the point that you might want to let some exceptions pass through, or you might want to handle different exceptions differently (e.g. show an error message for one, retry the operation for another). Some languages allow catching multiple types of exceptions without catching all exceptions. That can be a good compromise between a catch-all block and a very verbose "catch this, then this, then ..." block.
For example, consider the following block:
try {
// open a file, read some XML, and instantiate
// some classes referenced therein using reflection.
initializeModule();
}
catch (IOException e) {
// Do something
}
catch (ReflectiveOperationException e) {
// Do something
}
catch (XMLStreamException e) {
// Do something
}
Java 8 allows the following instead:
try {
// open a file, read some XML, and instantiate
// some classes referenced therein using reflection.
initializeModule();
}
catch (IOException | ReflectiveOperationException | XMLStreamException e) {
// Do something
}
This can be a good compromise because it catches specific exceptions in a single block without catching too much which might not be a clean design or the right place to catch other exceptions. For example, the code above lets RuntimeException classes (e.g. null pointers) pass up the call stack. This can be annoying to do in Java because RuntimeException inherits from Exception. Catching Exception will also catch RuntimeExceptions, which is not always what one wants to do.
The only time where I find catching all exceptions to be good practice is as a last resort before dying:
public static void main(String[] args) {
try {
// Invoke program logic here.
}
catch (Throwable t) {
// Last-ditch logging before letting the program die.
}
}