Lecture 19: Exception Handling

DJW pp.168-173.

Exception handling: Dealing effectively with exceptional conditions (usually errors of some kind).

Objectives:

  1. Keep the code for the elegant, interesting, algorithmic main (non-exceptional) flow of the program uncontaminated by the ugly, boring list of special cases that constitutes the exception handling.
  2. Provide for rapid unwinding of code. That is, in many cases, you're deep inside some sophisticated, complex algorithm, and then an error comes along that makes nonsense of the whole thing, and the only thing to throw the whole thing away. You want the low-level routine to be able to signal this without including a test for an error condition at each function call on the call sequence.
  3. The extreme, but common, case of (2) is that the error is irrecoverable; you have no idea what to do. In that case, you want the low-level routine to be able to signal that the program should be terminated with extreme prejudice.
  4. Patch things up in the course of unwinding. The abrupt termination is liable to leave data structures or even files or devices in an inconsistent state. If possible, these should be fixed.
  5. Transmit useful information through this unwinding of code. That is: successful error recovery often involves combining information from the low-level routine, where things went wrong, with information from the high-level routine, which knows the larger context. This is not a thing that programming languages are very good at; the whole thrust of top-down programming, information hiding etc. is that low-level routines do what they do without needing to know the high-level context, and that high-level routines can count on low-level routines doing the right thing without knowing how that is implemented. Exception handling features often include some mechanism for this; but it is far from a solved problem. This is one of the fundamental reasons why error messages from complex software (e.g. compilers) tend to be hard to interpret.

Programming language design and software engineering is largely about making things elegant. Exception handling is pretty much inescapably ugly. The exception handling features give some leverage over this ugliness, but mostly it's still ugly.

Exception Handling in Java

User-defined class of exception

class MyEmptyStack extends Exception {}

Throwing (raising) an exception

throw new MyEmptyStack()

Catching an exception

try  {  block1 }
  
{ catch (arg) block  }   Zero or many of these.
{ catch (arg) block  }
... 
{ catch (arg) block  }
{ finally  block }

When the try is executed, the body block1 is executed. If an exception is thrown while executing block1, then it jumps immediately to the catch clauses. The first catch clause whose argument class matches the class of the exception thrown is executed. (More detail below.)

The finally clause is always executed at the end of the try,

Useful for clean-up that must always be done; e.g. release a resource or close a file.

The exception thrown must be a subclass of the exception declaration of the catch, so that the variable in the catch can be bound to the exception thrown.

Finding the right catcher

If exception E is thrown in method M
  1. Work lexically out through try blocks in M to find a catch clause with a matching argument.
  2. If there is no such catch clause in M, then
    1. M terminates, after executing the applicable finally clauses:
    2. go to where M was called, in method P
    3. work lexically out through try blocks in P, looking for a matching catch clause
    4. Iterate back through callers.
  3. If you get back to main and no one has caught the error, then terminate.

See example ComplexCatcher.java

That is: The error is handled by the most specific (in terms of the calling sequence) method that knows how to deal with it.

Declaring a method P as throwing exception

If P either then P must be declared as throwing E.
public int P(int X) throws E { ... }

or

public int P(int X) throws E1, E2, E3 { ... }

If R throws but does not catch E, P calls R and P is declared as throwing E, then the call to R in P does not have to be in a try block.

Nice consequence for software engineering

Suppose that you are writing a mid-level routine Q. Q calls R which may throw an exception E, and Q is called by P.

Complex recovery

Suppose that P calls Q calls R calls S which throw exception E, and that there are now a whole collection of things that have to be done: S has the information to do some of them, R has the information to do some of them, Q has the information to do some of them, and P has the information to do some of them. There are two ways to deal with this. With both methods, all the calls involved and the throw have to be inside try blocks.

Runtime exceptions

Many library functions and built-in operators throw an exception of class Runtime Exception. Examples
Dividing by zero throws an ArithmeticException .
Applying Integer.parseInt(s) to a non-integer argument throws a NumberFormatException.

These are unchecked exceptions: that is, the calling function does not have to either catch them or declare itself as throwing them. (If they did, Java programming would be hopeless.) However, you can provide a catcher for them.

Programmers can define their own unchecked exceptions, by defining the exception to be an extension of RuntimeException. However, this is generally advised against.

IOException

A lot of the basic Java functions for reading from files or devices throw the exception IOException. This is a checked exception, so you have to provide a handler.

Examples

A generic stack that throws errors for popping an empty stack and overflowing.

ArrayStack.java

A tester for the stack:

Test1.java

A program for evaluating postfix expressions that uses this stack:

EvalPostfix.java

Reading from input with a handler for IOException.

wc.java

Complex structure of catchers:

ComplexCatcher.java