6. Error Handling - tuansondinh/clean_code GitHub Wiki

6.1 Use Exceptions Rather Than Return Codes

Don't use Return Codes. Reasons: "The caller must check for errors immediately after the call. Unfortunately, it’s easy to forget." S.104 Also we don't have clean separation between error handling and "normal" code.

BAD:

DeviceHandle handle = getHandle(DEV1); // Check the state of the device
if (handle != DeviceHandle.INVALID) {
  // Save the device status to the record field 
  retrieveDeviceRecord(handle);
} else{
  logger.log("Invalid handle for: " + DEV1.toString());
}

GOOD:

try {
  getHandle(DEV1);
} catch (HandleError e) {
  logger.log(e);
}

6.2 Start with writing try/catch/finally

"This helps you define what the user of that code should expect, no matter what goes wrong with the code that is executed in the try." S.105

6.3 Use Unchecked Exceptions

Don't use checked Exceptions. "The price of checked exceptions is an Open/Closed Principle violation. If you throw a checked exception from a method in your code and the catch is three levels above, you must declare that exception in the signature of each method between you and the catch. This means that a change at a low level of the software can force signature changes on many higher levels." S.107

6.4 Provide Context with Exceptions

"Each exception that you throw should provide enough context to determine the source and location of an error." S.107

  • use informative error messages containing the operation that failed and type of failure

6.5 Define Exception Classes in Terms of a Caller’s Needs

"... when we define exception classes in an application, our most important concern should be how they are caught." S.107

Following code contains a lot of duplication. (third-party library/API call) BAD:

ACMEPort port = new ACMEPort(12);
try { 
  port.open();
} catch (DeviceResponseException 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");
} finally {
  ...
}

Use a wrapper class "...that catches and translates exceptions thrown by the ACMEPort class" S.108

LocalPort port = new LocalPort(12); 
try {
  port.open();
} catch (PortDeviceFailure e) {
  reportError(e);
  logger.log(e.getMessage(), e);
} finally {
  ...
}

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);
    } 
  } ...
}

6.6 Define the normal Flow / Special Case Pattern

Using exceptions we have a nice separation between business logic and error handling. But be careful and avoid following mistake, where "...the exception clutters the logic." S.109

BAD

try {
  MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
  m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) { 
  m_total += getMealPerDiem();
}

Solution: Use the Special Case Pattern

"You create a class or configure an object so that it handles a special case for you. When you do, the client code doesn’t have to deal with exceptional behavior. That behavior is encapsulated in the special case object." S. 110

GOOD:

MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();

public class PerDiemMealExpenses implements MealExpenses {
 public int getTotal() {
 // return the per diem default
 }
}

Now in this example getTotal() will always return an object. If no meal -> object will return the per diem

6.7 Don't Return Null

If we return null, we need to always check for null. If we forget to check, the application can fail on us.

BAD:

List<Employee> employees = getEmployees();
if (employees != null) {
 for(Employee e : employees) {
 totalPay += e.getPay();
 }
}

SOLUTION:

"If you are tempted to return null from a method, consider throwing an exception or returning a SPECIAL CASE object instead." S.110

List<Employee> employees = getEmployees();
for(Employee e : employees) {
 totalPay += e.getPay();
}

public List<Employee> getEmployees() {
 if( .. there are no employees .. )
 return Collections.emptyList();
}

6.8 Don't Pass Null

"Unless you are working with an API which expects you to pass null, you should avoid passing null in your code whenever possible." S.111 -> to avoid run time errors, exceptions

6.9 Conclusion

"Clean code is readable, but it must also be robust.[...] We can write robust clean code if we see error handling as a separate concern." S.112

⚠️ **GitHub.com Fallback** ⚠️