Graduate Program KB

Clean Code

Chapter 7

  • Error handling should not obscure logic
  • Code bases should not be completely dominated by error handling

Exceptions

  • Use exceptions, not return codes

    • Return codes require the caller to check for errors immediately
    • Throwing exceptions makes code cleaner
    • Enforces single responsibility principle, decouples multiple types of error checking
  • Write the try-catch-finally statement first

    • When you execute code in try it means the execution could abort at any time
    • The runtime will resume within the catch statement
    • Helps define what the user should expect from that code, no matter what goes wrong
  • Use unchecked exceptions

    • Checked exceptions violate the open/closed principle
    • Throwing checked exceptions from a method where catch is a few levels above means that exception must be declared at each intermediate method
      • A change at a low level can force changes on multiple higher levels
  • Provide context with exceptions

    • There should be information regarding the source and location of the error
    • Create informative error messages
  • Define exception classes based on how exceptions should be caught

    • Chaining catch statements tend to have duplication because error handling is usually pretty standard despite the type of error
    • The code can be simplified by wrapping it and making sure it returns a common type of exception
    • A wrapper class just catches and translates exceptions
    ACME port = new ACMEPort(12);
    
    try {
        port.open();
    }
    catch(DeviceResponseException e) { reportPortError(e); }
    catch(ATM1212UnlockedException e) { reportPortError(e); }
    catch(GMXError e) { reportPortError(e); }
    
    LocalPort port = new LocalPort(12);
    
    try {
        port.open();
    }
    catch(PortDeviceFailure e) { reportError(e); }
    
    /* Wrapper */
    public class LocalPort {
        /* Constructor to initialise port with ACME */
    
        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); }
        }
    }
    
  • The idea is to push error detection to the edges of the program

    • We wrap external APIs to throw our own exception
    • Define handlers at a high level so it can deal with any aborted computation
    • However, sometimes you have to create a class or configure an object to handle a special case for you (called the Special Case Pattern)
      • The client code shouldn't have to deal with exceptions, that behaviour is encapsulated in the special case object

Null

  • Don't return null

    • Creates more work for ourselves by having to check for null
    • A missed null check would potentially mean an uncaught NullPointerException from the depths of your application
    • The issue is there are too many null checks, consider throwing exceptions or returning special case objects instead
  • Don't pass null

    • Unless an API expects a null pass, avoid passing null to functions
    • There's not really a good way to deal with passing null except forbidding it in the first place
      • Could check for null then throw our own exception, but then we have to define a handler for it
      • Could use a set of assertions which provides good documentation, but doesn't solve the problem of getting a runtime error