Graduate Program KB

Chapter 7 Notes


Error Handling

  • Error handling is something everyone has to do, it is not related to your level of programming ability.
  • Input could be incorrect, or devices could fail.
  • Whilst it is important, error handling should not obscure logic.

Use Exceptions Rather Than Return Codes

  • As demonstrated in figure 7-1, error flags are set, with matching logger statements to catch errors.
    • These are bad as the caller must check for errors immediately after the call
  • Listing 7-2 demonstrates a much cleaner approach which uses exceptions from external methods
    • Wrapping in a try/catch block also looks cleaner
  • The two concerns are now separated (algorithm for device shutdown and error handling)
// Listing 7-1
public class DeviceController {
 // ...
 public void sendShutDown() {
    DeviceHandle handle = getHandle(DEV1);
        // Check the state of the device
    if (handle != DeviceHandle.INVALID) {
        // Save the device status to the record field
        retrieveDeviceRecord(handle);
        // If not suspended, shut down
        if (record.getStatus() != DEVICE_SUSPENDED) {
            pauseDevice(handle);
            clearDeviceWorkQueue(handle);
            closeDevice(handle);
        } else {
            logger.log("Device suspended. Unable to shut down");
        }
        } else {
            logger.log("Invalid handle for: " + DEV1.toString());
        }
    }
// ...
}

// Listing 7-2
public class DeviceController {
 ...
    public void sendShutDown() {
        try {
            tryToShutDown();
        } catch (DeviceShutDownError e) {
            logger.log(e);
        }
    }

    private void tryToShutDown() throws DeviceShutDownError {
        DeviceHandle handle = getHandle(DEV1);
        DeviceRecord record = retrieveDeviceRecord(handle);
        pauseDevice(handle);
        clearDeviceWorkQueue(handle);
        closeDevice(handle);
    }

    private DeviceHandle getHandle(DeviceID id) {
        // ...
        throw new DeviceShutDownError("Invalid handle for: " + id.toString());
        // ...
    }
    // ...
}

Write Your Try-Catch-Finally Statement First

  • Exceptions define a scope within our program. Executing code within a try block, we are stating that execution can abort at any point and then resume at the catch.
  • Our Catch statement in turn must leave the program in a consistent state, regardless of execution in the try block.
    • This is why we should use a try-catch-finally block for code which could throw an exception.
  • In the provided example, we first write a test case which expects an exception to be thrown for an incorrect filename.
    • The next step is to implement the method functionality so that an exception is thrown within a catch block
    • Now that the exception is 'loaded', we can implement the rest of the functionality in the try block under test
    • This helps to maintain the transactional nature of the scope
@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
    sectionStore.retrieveSection("invalid - file");
}
public List<RecordGrip> retrieveSection(String sectionName){
    try{
        FileInputStream stream = new FileInputStream(sectionName);
        stream.close();
    } catch (FileNotFoundException e){
        throw new StorageException("Retrieval Error", e);
    }
    return new ArrayList<RecordGrip>();
}

Use Unchecked Exceptions

  • Method signatures would list all types of exceptions it could pass to it's caller. Your code would not compile if the signature didn't match what your code could do.
  • No longer needed for production of robust software, this is demonstrated through many popular programming languages (such as C#, Python and Ruby) not having checked exceptions.
  • A price comes with checked exceptions. It violates the Open/Closed principle (code should bne open for extension, but closed for modification).
    • If we throw a checked exception, but the catch block is 3 levels above, we would have to declare the exception in the signature of each method between you and the catch block.
    • Low level changes can force signature changes on higher levels.

Provide Context with Exceptions

  • Thrown exceptions should be useful.
  • Informative error messages are imperative.
    • They should provide the name of the operation, and the type of failure.
    • If logging, provide enough information to be able to log information in your catch block.

Define Exception Classes in Terms of a Caller's Needs

  • There are multiple ways to classify an error. Based on their source, or their type primarily.
  • For exceptions, our biggest concern is how they are caught.
    ACMEPort port = new ACMEPort(12);

    try {
        port.open();
    } catch (DeviceResponseExecution e) {
        reportPortError(e);
        logger.log("Device response exception", e);
    } catch (ATM1212UnlockedException, e){
        reportPortError(e);
        logger.log("Unlock exception", e);
    } catch (GMXError e){
        reportPortError(e);
        logger.log("Device response exception", e);
    } finally {
        // ...
    }
  • There is lots of duplicated code in this example (primarily that of calls to reportPortError()).
  • Due to our execution being the same regardless of the type of exception thrown, we can simplify this code.
    LocalPort port = new LocalPort(12);
    try{
        port.open();
    } catch (PortDeviceFailure e){
        reportError(e);
        logger.log(e.getMessage(), e);
    } finally {
        // ...
    }
  • In this example, our LocalPort class is a wrapper which catches and translates exceptions thrown by the ACMEPort class.
  • Wrapping third-party API's like this is best-practice.
    • It allows us to define an API we like instead of being tied to a vendor.
    • Mocking and changing libraries also becomes less painful.
public class LocalPort{
    private ACMEPort innerPort;

    public LocalPort(int portNumber){
        innerPort = new ACMEPort(portNumber);
    }

    public void open() {
        try{
            innerPort.open();
        } catch (DeviceResponseException e) {
            throw new PortDeviceFailure(e);
        } catch (ATM1212UnlockedException e) {
            throw new PortDeviceFailure(e);
        } catch (GMXError e) {
            throw new PortDeviceFailure(e);
        }
    }
    // ...
}

Define the Normal Flow

  • Following the previous steps allows us to separate our error handling from the rest of the program. Sometimes we may be hit with a situation like below.
  • In this example, if meals are expenses, they become part of the total, otherwise get a meal per diem amount (daily allowance for food).
try {
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
} catch (MealExpensesNotFound e) {
    m_total += getMealPerDiem();
}
  • This could be changed to something simpler.
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
  • This works if we change the ExpenseReportDAO to always return a MealExpense object. If there are no meal expenses, a MealExpense object is returned, which returns the per diem as its total.
public class PerDiemMealExpenses implements MealExpenses{
    public int getTotal() {
        // return the per diem total
    }
}

Don't Return Null

  • An example of needing to check for null:
public void registerItem(Item item) {
    if(item != null) {
        ItemRegistry registry = persistentStore.getItemRegistry();
        if(registry != null){
            Item existing = registry.getItem(item.getID());
            if(existing.getBillingPeriod().hasRetailOwner()){
                existing.register(item);
            }
        }
    }
}
  • Working in a code base with many null-checks can be dangerous. It only takes one missed check to send an application spiralling.
  • Instead of returning null from a method, instead we should throw an exception or return a special case object instead. Example:
    List<Employee> employees = getEmployees();
    if(employees !- null) {
        for(Employee e: employees){
            totalPay += e.getPay();
        }
    }
  • We can modify getEmployee so that it will return an empty list rather than null.
  • We then no longer need the check to see if employees is null.
    public List<Employee> getEmployees() {
        if(/* No employees*/) {
            return Collections.emptyList();
        }
    }

Don't Pass Null

  • Passing null to methods is worse than returning null from a method (unless required).
public class MetricsCalculator{
    public double xProjection(Point p1, Point p2){
        return (p2.x - p1.x) * 1.5;
    }
}
  • Passing in null as an argument would result in a NullPointerException.

    • We could mitigate this with a new exception type and throw it when needed.
    • Even better, we could use a set of assertions to check if the arguments are null.
  • In most programming languages, there is no good way to deal with a null which is passed accidentally.

    • We can forbid passing of null, so that we know an error has happened if this occurs.
  • Examples of how to mitigate this:

public class Metrics Calculator {
    public double xProjection(Point p1, Point p2){
        if (p1 == null || p2 == null){
            throw InvalidArgumentException(
                "Invalid argument for Metrics Calculator.xProjection");
        }
        return (p2.x - p1.x) * 1.5;
    }
}

// OR

public class MetricsCalculator{
    public double xProjection(Point p1, Point p2){
        assert p1 != null: "p1 should not be null";
        assert p2 != null: "p2 should not be null";
        return (p2.x - p1.x) * 1.5;
    }
}

End of Chapter 7.