As the programmer, you must know what constitutes a bad result, and what it means. It’s often awkward to work around the limitations of passing error values in the normal path of data flow. An even worse problem is that certain types of errors can legitimately occur almost anywhere, and it’s prohibitive and unreasonable to explicitly test for them at every point in the software.
Java offers an elegant solution to these problems with exception handling. An exception indicates an unusual condition or an error condition. Program control becomes unconditionally transferred or “thrown” to a specially designated section of code where it’s caught and handled. In this way, error handling is somewhat orthogonal to the normal flow of the program. We don’t have to have special return values for all our methods;errors are handled by a separate mechanism. Control can be passed long distance from a deeply nested routine and handled in a single location when that is desirable, or an error can be handled immediately at its source.
An Exception
object is created by the code at the point where the error condition arises. It can hold whatever information is necessary to describe the exceptional condition, optionally including a full stack trace for debugging. The Exception
object is passed as an argument to the handling block of code, along with the flow of control. This is where the terms “throw” and “catch” come from: the Exception
object is thrown from one point in the code and caught by the other, where execution resumes.
Exception Handling
The try/catch
guarding statements wrap a block of code and catch designated types of exceptions that occur within it:
try { readFromFile("foo"); ... } catch ( Exception e ) { // Handle error System.out.println( "Exception while reading file: " + e ); ... }
In this example, exceptions that occur within the body of the try
portion of the statement are directed to the catch
clause for possible handling. The catch
clause acts like a method; it specifies an argument of the type of exception it wants to handle and, if it’s invoked, it receives the Exception
object as an argument. Here we receive the object in the variable e
and print it along with a message.
A try
statement can have multiple catch
clauses that specify different types (subclasses) of Exception
:
try { readFromFile("foo"); ... } catch ( FileNotFoundException e ) { // Handle file not found ... } catch ( IOException e ) { // Handle read error ... } catch ( Exception e ) { // Handle all other errors ... }
The catch
clauses are evaluated in order, and the first possible (assignable) match is taken. At most, one catch
clause is executed, which means that the exceptions should be listed from most specific to least. In the previous example, we’ll anticipate that the hypothetical readFromFile( )
can throw two different kinds of exceptions: one that indicates the file is not found; the other indicates a more general read error. Any subclass of Exception
is assignable to the parent type Exception
, so the third catch
clause acts like the default
clause in a switch
statement and handles any remaining possibilities.
One beauty of the try/catch
scheme is that any statement in the try
block can assume that all previous statements in the block succeeded. A problem won’t arise suddenly because a programmer forgot to check the return value from some method. If an earlier statement fails, execution jumps immediately to the catch
clause; later statements are never executed.
Bubbling Up
What if we hadn’t caught the exception? Where would it have gone? Well, if there is no enclosing try/catch
statement, the exception pops to the top of the method in which it appeared and is, in turn, thrown from that method up to its caller. If that point in the calling method is within a try
clause, control passes to the corresponding catch
clause. Otherwise the exception continues propagating up the call stack. In this way, the exception bubbles up until it’s caught, or until it pops out of the top of the program, terminating it with a runtime error message. There’s a bit more to it than that because, in this case, the compiler would have reminded us to deal with it, but we’ll get back to that in a moment.
Throwing Exceptions
We can throw our own exceptions: either instances of Exception
or one of its existing subclasses, or our own specialized exception classes. All we have to do is create an instance of the Exception
and throw it with the throw
statement:
throw new Exception( );
Execution stops and is transferred to the nearest enclosing try/catch
statement. An alternative constructor lets us specify a string with an error message:
throw new Exception("Something really bad happened");
The finally Clause
What if we have some cleanup to do before we exit our method from one of the catch
clauses? To avoid duplicating the code in each catch
branch and to make the cleanup more explicit, use the finally
clause. A finally
clause can be added after a try
and any associated catch
clauses. Any statements in the body of the finally
clause are guaranteed to be executed, no matter why control leaves the try
body:
try { // Do something here } catch ( FileNotFoundException e ) { ... } catch ( IOException e ) { ... } catch ( Exception e ) { ... } finally { // Clean up here }
In this example, the statements at the cleanup point will be executed eventually, no matter how control leaves the try
. If control transfers to one of the catch
clauses, the statements in finally
are executed after the catch
completes. If none of the catch
clauses handles the exception, the finally
statements are executed before the exception propagates to the next level.