Previous: Objects and Data Structures
- At times it's impossible to see what the code does because all of the scattered error handling.
- Error handling is important but if it obscures logic it's wrong.
- Create separation between the business logic and error handling
Bad:
public void sendShutDown() {
DeviceHandle handle = getHandle(DEV1);
if (handle != DeviceHandle.INVALID) {
...
if (record.getStatus() != DEVICE_SUSPENDED) {
...
} else {
logger.log("Device suspended");
}
} else {
logger.log("Invalid handle");
}
}
Good:
public void sendShutDown() {
try {
tryToShutDown();
} catch (DeviceShutdownError e) {
logger.log(e);
}
}
private void tryToShutDown() throws DeviceShutDownError {
...
throw new DeviceShutDownError("Invalid ID for...");
...
}
It's easier to read and separates the algorithm for device shutdown and the error handling.
- Exceptions define a scope within your program.
- Helps you define what the user of that code should expect.
- Try to write tests that force exceptions and then add behavior to your handler to satisfy your tests. This will help you build the transaction of the
try
block first.
- Checked exceptions violate the Open/Closed principle.
- In the calling hierarchy of a large system high level functions call lower level functions and so on. If a low-level function changes and now must throw a checked exception a
throws
clause must be added to it's signature, meaning every higher calling function now must catch the exception or append it's ownthrows
clause. - Checked exceptions break encapsulation becuase all functions in the path of a throw must know about details of that low-level exception.
Create informative error messages, provide enough context to determine the source and location of the error.
Should be classified by how they are caught
Bad:
AMCEPort port = new AMCEPort(12);
try {
port.open()
} catch (DeviceResponseException e) {
// report, log...
} catch (AMT1212UnlockedExcpetion e) {
// report, log...
} ...
Good:
LocalPort port = new LocalPort(12);
try {
port.open()
} catch (PortDeviceFailure e) {
// report, log...
} ...
LocalPort
is just a wrapper that catches and translates exceptions thrown by the ACMEPort
class.
Wrap third-party API's:
- Allows for cleaner and easily testable code
- Minimizes dependencies upon it
- Not tied to a vendors design choices
Sometimes you don't want to abort in the catch
statement and still want to continue business logic
Bad:
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch (MealExpensesNotFound e) {
m_total += getMealPerDiem();
}
Good:
Make getMeals
should always return a MealExpenses
object. Using a special case pattern could return a MealExpenses
object subclass with a getTotal
method that handles the per diem logic which would leave you with just:
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
- Returning null invites errors, i.e.
- Returning null creates future work for all the times you then have to check for null with
if (item != null)
- Consider throwing an exception or returning a special case object
- If it's a third-party API that returns null consider wrapping that method to throw an exception or return a special case object
Bad:
public double xProjection(Point p1, Point p2) {
return (p2.x - p1.x) * 1.5;
}
If you pass null such as xProjection(null, new Point(12, 13));
we'll have a runtime error
- Throwing an exception means you'll just have to define a handler anyway
- Assertions make the trouble understandable, but still causes a runtime error
- The rational approach is to forbid passing null by default
We can write clean robust code if we see error handling as a separate concern, something that is viewable independently of our main logic.