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.