21. Exceptions

Key concepts

  1. Handling Errors
  2. Error Codes
  3. Exceptions

Introduction

Strategies for handling errors depend largely on the nature of the application. Since beginning programming courses typically deal in trivial applications, error handling is often dismissed as an unimportant subject. In real world applications, however, doing the "right thing" in the face of errors is critically important.

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.

One approach: Error codes

Recall the BankAccount class. We handled an insufficient funds condition by returning an "error code". Here's the relevant fragment of the class definition for the BankAccount:
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.

A Better Approach: Exceptions

Exceptions allow an implementor of a class to defer the responsibility of handling an error to the client. In this way, they are similar to error codes. However, exceptions have two important advantages over error codes. First, the compiler will enforce handling of the error. This means that exceptions, unlike error codes, cannot be ignored. Second, in handling exceptions, clients may wrap up segments of potentially error generating code and handle classes of errors in one central location.

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.

Handling Exceptions

We've seen an example of throwing an exception, so now let's look at how we might handle one:
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.

Class Design Issues

What if we call an exception throwing method in the body of a method? We have two options. The first is to handle it inside of the method, the other is just to declare that it is thrown. In the case of the BankAccount, the latter strategy is best. We just "percolate" the exception onward, and force the client to deal with it. Here's an example of the first approach.
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.


Ben Dugan & UW-CSE, Copyright (c) 2001.