There are a variety of strategies for handling errors at run time. These include doing nothing, notifying the user, halting the program, and a host of other possible actions. Depending on the type of application, various of these approaches may be valid.
At one extreme, imagine a program that averages weather data over extremely large data sets. The program designers probably expect out of range or missing data, and would design their program to be silently tolerant of such errors, perhaps reporting the number of missing or invalid data items. Halting the program or issuing a warning for every invalid data item would certainly be overkill in this case.
In the middle ground, imagine an ATM application. If for instance, the user attempts to overdraw your bank account, halting the ATM program (and shutting down the ATM) is probably not the right approach. Silently ignoring the problem is probably not the right strategy either. Ideally, the program should give them a warning, and not modify their account. It may then want to give the user another opportunity to withdraw less money.
At the other extreme, there exist applications whose failure can put many lives at risk. Imagine a program that controls a nuclear reactor or assists air traffic controllers. If the program that controls a nuclear reactor encounters an input that is out of the expected range, it may mean that something very serious has happened. Silently ignoring it is certainly not an option. Notifying human operators is certainly of utmost importance. Shutting down the program is perhaps even the right strategy, depending on how the entire control system has been designed.
It's the job of a class designer and implementor to decide what to do about errors, and how to report and handle them if necessary. The conditions that are considered errors, and the behavior of classes of objects in the face of these errors are important details of a class interface.
public class BankAccount { private int number; private double balance; private String name; /** Decrease the balance by the given amount. If insufficient funds, the balance is unchanged. @param amount the amount to withdraw @return true if and only if sufficient funds were available */ public boolean withdraw(double amount) { if (amount <= this.balance) { this.balance = this.balance - amount; return true; } else { return false; } } }Also, recall the example of how we can use the error code that is returned in order to deal with errors such as these:
BankAccount account = new BankAccount(1234, 225.34, "Bill"); Input input = new Input(); double amount = input.readDouble("How much money do you want?"); boolean succeeded = account.withdraw(amount); if (succeeded) { System.out.println("Here's your money!!"); } else { System.out.println("Sorry, insufficient funds!"); }The error code places the onus of dealing with the error onto the client of the BankAccount class. This is fundamentally the right strategy, but has shortcomings: First, it means that to really do the right thing, client code must constantly capture and check the returned error code for every operation. Unfortunately, programmers are fundamentally pretty lazy people, and error codes are frequently ignored. Second, if many methods in a class return error codes, it becomes tedious to check for them, each and every time they might be meaningful.
First, let's look at an example of raising or throwing an exception.
public class BankAccount { // ... other stuff ... /** Decrease the balance by the given amount. If insufficient funds, throws an Exception. @param amount the amount to withdraw */ public void withdraw(double amount) throws Exception { if (amount <= this.balance) { this.balance = this.balance - amount; return true; } else { throw new Exception("Insufficient Funds"); } } }The above example introduces two new keywords,
throw
and
throws
. Think of throw
as similar to the
keyword return
. It alters the flow of control of the
program. After throw
we must provide an expression that
evaluates to an exception object. In Java, exceptions are represented
as objects.
The keyword throws
modifies the signature of the method,
advertising to the world that this method may, under some
circumstances throw an exception. If the body of the method contains
a throw statement, or calls a method that may throw an exception that
does not get handled inside of the body of the method, then the
compiler will insist that this exception throwing behavior be
advertised in the method signature. By this manner, the compiler can
and does enforce the handling of exceptions in client code.
BankAccount account = new BankAccount(1234, 225.34, "Bill"); Input input = new Input(); double amount = input.readDouble("How much money do you want?"); try { account.withdraw(amount); System.out.println("Here's your money!!"); // give the user their money.... } catch (Exception anException) { System.out.println("Sorry, insufficient funds!"); }The new bit of syntax that we're introducing here is called a
try-catch
block. We can use the try-catch block to wrap
up a method call that may throw an exception. If an exception is
thrown by any statement in that block, control passes to the
catch
block, where we can place code to handle the
exception.
Think about what actually happens if the exception is thrown by the withdrawal operation. Do the subsequent lines in the try block get excecuted? We would hope not, because this would mean giving the user money that didn't exist in their account. This behavior might make you and me happy, but not the banker. In fact, the subsequent lines do not get executed, because the thrown exception causes the control of the execution of the program to change. Code is no longer excecuted sequentially; rather, the execution of the program is literally suspended and control is passed to the enclosing catch block. This is substantially different from the standard form of program execution. But if you think about it, not at all unreasonable: erroneous conditions should cause our program to halt normal operation and do some work to fix or report the condition.
public class BankAccount { // ... other stuff ... /** Decrease the balance by the given amount. If insufficient funds, throws an Exception. @param amount the amount to withdraw */ public void withdraw(double amount) throws Exception { if (amount <= this.balance) { this.balance = this.balance - amount; return true; } else { throw new Exception("Insufficient Funds"); } } /** Transfer money between accounts. If the account to transfer from has insufficient funds, an Exeption is thrown. @param other a BankAccount to tranfer from @param amt an amount to transfer */ public void transferFrom(BankAccount other, double amt) throws Exception { other.withdraw(amt); this.deposit(amt); } }The
transferFrom
method calls the withdraw
method. Of course, that method may throw an exception. The simple
approach taken here is just to declare that fact by noting it in the
method signature for transferFrom
. This approach keeps
with our general strategy for this class: force the client to deal
with errors relating to insufficient funds.
Now let's look briefly at the other approach, which handles the exception within a method body.
public class BankAccount { // ... other stuff ... /** Decrease the balance by the given amount. If insufficient funds, throws an Exception. @param amount the amount to withdraw */ public void withdraw(double amount) throws Exception { if (amount <= this.balance) { this.balance = this.balance - amount; return true; } else { throw new Exception("Insufficient Funds"); } } /** Transfer money between accounts. Print a message if the origin account has insufficient funds. @param other a BankAccount to tranfer from @param amount an amount to transfer */ public void transferFrom(BankAccount other, double amt) { try { other.withdraw(amt); this.deposit(amt); } catch (Exception anException) { System.out.println("Insufficient funds."); } } }Notice that because we are handling the exception in the body of
transferFrom
, we no longer need to modify the method
signature of that method.
This approach is undesirable on at least two counts. First, the
methods of the class are inconsistent in terms of their error
behavior. The withdraw
method may throw an exception if
there are not enough funds, but the transferFrom
method
simply prints out a warning. Consistency in class design is a
principle of utmost value, and the above design violates that
principle.
Second, the above example demonstrates the potential drawback of handling errors at this level. BankAccounts are data objects, they have basic properties and behaviors, but it's hard to argue that the BankAccount should be responsible for printing an error message. BankAccounts are certainly responsible for doing the right thing in the face of insufficient funds, which is to not alter the account balance and to let the client code know that an error has occurred. Imagine a variety of banking systems that use different display technologies yet all interface with the same BankAccount objects. One system might wish to display a graphical icon if an account is overdrawn; another system may want to display a pop-up dialog; another system may want to print a message on a piece of paper; systems in Germany will want to display their messages in a different language from those in England. The display of an error message to the user is an issue for the designer and implementor of the user interface. The above design violates this partitioning of responsibility, and results in a less reusable class design.