April 5, 2015

6 rules of exception handling by example (part 2 of 2)


In this blog post, I present six rules I consider crucial to exception handling design. I will illustrate each one of them with an anti-pattern and a “best practice” solution in order to show the trouble caused by improper exception handling. Make sure to check out the first part here.

Checked exceptions for business errors; unchecked exceptions for technical errors

The differentiation of checked and unchecked exceptions still seems to cause trouble to many developers, at least in my experience, and this is very bad. If you do this one wrong, you may end up with a codebase which is even harder to maintain than if any other of these anti-patterns are applied.

I will not cover here the technical differentiation of checked and unchecked exceptions, as this is covered exhaustively by basic Java tutorials. Instead, let me again explain how to implement proper exception handling design by comparing the anti-pattern and the best practice approach in action.

First of all, here again is the rule to which we must obey:
  • Use checked exceptions (sparingly) for business errors: These are expected errors that the caller can and must handle.
  • Use unchecked exceptions for technical errors: These are either unexpected errors that the caller cannot handle sensibly other than throwing them up in the call chain; or they are exposing the caller to technical internals he is not interested in or not even allowed to know.
It is however critical to understand that the notion of what is “business logic” and what is “technical logic” is highly context-dependent. Depending on the layer of abstraction you’re working on, this attribution will change. If, for example, you’re working on file access logic, retrieving a file clearly is business logic whereas when you’re working on a chat application which as a side effect writes application log messages to a log file, accessing this file may be a technical issue.

The anti-pattern

Terrible things happen if you do not follow this rule. Let’s observe this example we're already familiar with:
public class CheckedUncheckedClass {
    private static final String FILE_PATH = "my/file/path.txt";
    
    private static File tryLoadFile() throws FileNotFoundException {
        // let's assume file loading failed
        boolean fileLoadedSuccessfully = false;
        
        if (fileLoadedSuccessfully) {
            return new File(FILE_PATH);
        }
        else {
            throw new FileNotFoundException("Could not load file with path " + FILE_PATH);
        }
    }
    
    public static String getFileName() throws FileNotFoundException {
        File file = tryLoadFile();
        return file.getName();
    }
}

Let’s say this example applies a “use checked exceptions wherever possible” policy. In my experience, it’s a widespread misconception that using checked exceptions increases application robustness. Much in the contrary, this strategy can seriously diminish robustness whilst increasing component interdependence.

Here, tryLoadFile() throws a FileNotFoundException, which is a checked exception. This is reasonable. Here, file handing is business logic. In the getFileName() method, however, according to this class’s exception handling policy, that checked exception is thrown up to the caller. This has two undesired consequences:
  • The conceptual problem: The caller of getFileName() is exposed to internals of file handling he should not need to know.
  • The software design problem: The caller cannot sensibly react to that exception. According to that policy, he can now only either throw it up further, or process it, e.g. by logging.
If the client opts for throwing the exception up, the problem really is just shifted up on the stack. This reveals the true design dept of checked exceptions: They create implicit interfaces. The entire call stack is linked by the throws declarations. This really is a viral behavior by design. It maximizes coupling and diminishes maintainability. Changing the exception type one day (e.g. using an unchecked type instead) would break the complete call chain and every class involved. This is truly not an option.

If, on the other hand, the client decides to process the exception, e.g. by logging it, things get even worse. Let’s assume that the client implements exception logging like so:
public class CheckedUncheckedCaller {
     public void callBusiness() {
          String fileName;
          try {
                fileName = CheckedUncheckedClass.getFileName();
          } catch (FileNotFoundException ex) {
                DummyExceptionHandler.showMessage(ex.getMessage());
          }
     }
}

At the very moment he logs the exception, any information about it is lost. If the CheckedUncheckedCaller class is not the root class of our entire application which we assume it’s not, any subsequent caller of CheckedUncheckedCaller#callBusiness() would have no information about whether that method executed successfully or whether a FileNotFoundException occurred. Yes, logging an exception actually is equivalent to swallowing an exception! If you’re lucky, your logging works and prints the stack trace to the application / server log, but potentially, there’s no trace left of your exception. Also, this code is not testable as the test (just as any caller) would have no information about the exception occurrence.

But here’s the thing: Let’s imagine a NullPointerexception happened somewhere inside the callBusiness() execution. As it’s an unchecked exception, it would actually run through your entire call stack (given you didn’t catch it somewhere) and thus trigger proper exception handling, namely crashing the application (or rather, trigger some central logging. See the last section about AOP for details). This means that unexpected, unchecked exceptions would actually trigger proper exception handling whilst the exceptions you are painstakingly observing will get lost. This is less than unpleasant.

The solution

We already discussed in detail how both of the following strategies are foul when dealing with checked exceptions:
  • Throwing them up. This couples caller and callee tightly by an implicit interface.
  • Processing them, e.g. logging them. This is essentially exception swallowing.
Thus, the only option left is really to wrap the checked exception into an unchecked exception and throw it up, as in this example implementation:
public static String getFileName() {
 File file;
 try {
  file = tryLoadFile();
 } catch (FileNotFoundException ex) {
  throw new RuntimeException(ex);
 }
 return file.getName();
}

Here, information about the original exception is preserved, but any subsequent caller is decoupled from getFileName() internals.

Note that this is an idealized example. What I really wanted to show here is that using checked exceptions should not be the prior solution. However, checked exception can be used sensibly when it comes to actually mark business errors. They can warn the client that he must watch out for a well-defined error postcondition and react to it in a well-defined way.

Let’s observe this example as a “checked exceptions best practices” implementation:
public class CheckedClass {
     private void validate(String input) throws ValidationException {
          if (input == null) {
                throw new ValidationException(input);
          }
     }
     
     public void callBusiness() {
          try {
                validate(null);
          } catch (ValidationException ex) {
                DummyExceptionHandler.showMessage(ex.getMessage());
          }
     }
}

Here, the validate(String) method throws a ValidationException which is a checked exception. This allows callBusiness() to carefully watch for this exception and react in a well-defined way. (Note that for simplicity reasons, callBusiness() is thought to be the top of the interaction layer and thus responsible to process exceptions before they freely break in the UI layer.)

Also consider one last thing: After Java, no other major programming language ever used checked exceptions again. Programming language exception design still is a field of active research, but checked exceptions have proved to mislead to flawed software design. You should shield your application design from being lead by such a complex and disputed concept.

Fine-grained exception control

This rule states that you should use the most specific exception class possible for both throwing and catching an exception to allow for fine-grained exception control. Violations to this rule are typically related to other rule violations such as the usage of magical error codes, and swallowing exceptions.

The anti-pattern

As an extreme anti-pattern example let’s assume a project follows the policy to use “one single exception class at a layer boundary”. To make things even more severe, let’s assume this exception is designed as a checked type.

Let’s take a look at this example code:
public class FineGrainedClass {
     private static final String FILE_PATH = "my/file/path.txt";
     
     private static File tryLoadFile() throws FileNotFoundException {
          // let's assume file loading failed
          boolean fileLoadedSuccessfully = false;
          
          if (fileLoadedSuccessfully) {
                return new File(FILE_PATH);
          }
          else {
                throw new FileNotFoundException("Could not load file with path " + FILE_PATH);
          }
     }
     
     public static String getFileName(String input) throws AllmightyException {
          if (input == null) {
                throw new AllmightyException("Input must not be null");
          }
          File file;
          try {
                file = tryLoadFile();
          } catch (FileNotFoundException ex) {
                throw new AllmightyException(ex);
          }
          return file.getName();
     }
}

Let’s assume that the getFile(String) method marks the layer boundary. Thus, as to our policy, throwing our single exception type AllmightyException is enforced.

Now, for the caller, an AllmightyException could imply that either the file loading failed or that the getFileName(String)’s not-null prepondition was violated. The caller’s only hope to find out the actual error source would be if our exception type encapsulates reasonable information about its source. This would then be a violation of using magical error codes, again.

Note also that this particular policy cannot be technically guaranteed as you have no control over RuntimeExceptions or Errors (trying to capture them would undermine any of Java’s exception handling mechanics).

The solution

The solution of course is obvious: Try to use the most specific exception class both in the throws as well as in the catch block, as in this improved example:
public class FineGrainedClassFixed {
     private static final String FILE_PATH = "my/file/path.txt";
     
     private File tryLoadFile() throws FileNotFoundException {
          // let's assume file loading failed
          boolean fileLoadedSuccessfully = false;
          
          if (fileLoadedSuccessfully) {
                return new File(FILE_PATH);
          }
          else {
                throw new FileNotFoundException("Could not load file with path " + FILE_PATH);
          }
     }
     
     public String getFileName(String input) {
          if (input == null) {
                throw new IllegalArgumentException("'input' must not be null.");
          }
          File file;
          try {
                file = tryLoadFile();
          } catch (FileNotFoundException ex) {
                throw new RuntimeException(ex);
          }
          return file.getName();
     }
}

Here, we also used unchecked exceptions only. As this example class represents a layer boundary, it’s most natural for the upper layer not to care about internals of this layer – this thus fulfils the use case of technical, hence unchecked, exceptions.

It’s another best practice, too, to stick with the JDK’s exceptions whenever possible to keep your API standardized. Introduce your own exception type only if you cannot express the cause of that exception semantically coherent with any of the built-in exception types.

Use AOP

The anti-pattern

Exception handling in a so called cross-cutting concern, that is, it is common to all layers of a software. In order not to violate the DRY principle, such aspects must be implemented in one central place. There are so-called aspect-oriented frameworks (as integrated in Java EE or Spring) for that purpose, or your technology stack may offer specific means of centralizing exception handling (such as Java Web exception pages or servlet filters). Making use of those mechanics must be key to your application’s overall exception handling strategy. Scattering exception handling over multiple layers not only violates DRY, but is the typical root cause of the “swallowing exceptions” anti-pattern we previously discussed.

Consider this example:
public class CccClass {
     private static final String FILE_PATH = "my/file/path.txt";
     
     private File tryLoadFile(String fileName) throws FileNotFoundException {
          // simulate NullpointerException
          if (fileName == null) {
                throw new NullPointerException();
          }
          // let's assume file loading failed
          boolean fileLoadedSuccessfully = false;
          
          if (fileLoadedSuccessfully) {
                return new File(FILE_PATH);
          }
          else {
                throw new FileNotFoundException("Could not load file with path " + FILE_PATH);
          }
     }
     
     public String getFileName(String input) {
          if (input == null) {
                Logger.getLogger(CccClass.class.getName()).log(Level.SEVERE, "Input must not be null");
          }
          File file;
          try {
                file = tryLoadFile(input);
          } catch (FileNotFoundException ex) {
                throw new RuntimeException(ex);
          }
          return file.getName();
     }
}

Here, getFileName(String) implements its own exception handling for the case where input is null. Exception handling is implemented here as a logger call. On the other hand, other exceptions such as a file loading error would be thrown up in order to be handled by the caller.

As you can see, this just resolves to another illustration of the point I previously presented that logging an exception means swallowing an exception. Imagine what happens if input actually is null. The respective message is logged, but the program just keeps going; in this example, it would trigger a NullpointerException within the tryLoadFile(String) call, which would just be a side effect of improper exception handling. Everything we said about swallowing exception practices applies again. The original exception is lost.

The solution

Now the solution to that problem is straightforward again. Throw up any exception and implement exception handling in exactly one single place. I will not print the solution listing here as it’s literally identical to the FineGrainedClassFixed solution of the previous section.

Conclusion

Obeying the technical specifications of Java’s exception handling is not enough to build robust, safe, and maintainable applications. I see quite a lot of misconceptions and anti-patterns in practice. Here I presented six rules of exception handling which I think of as forming crucial and convenient guidelines for proper exception handling design. These six rules come in handy for code reviews as well. If you make one or more ticks for rule violations, the code is at least fishy:
  1. Magical error codes
  2. Return null on error
  3. Swallow exception
  4. Checked / unchecked
  5. Fine-grained exceptions
  6. AOP
I am highly interested in your opinion on this topic. Do you agree with these six rules? Have I missed other crucial rules which might lead to terrible code design flaws when violated? Please let me know your thoughts in the comments section below.

The complete code examples are available on GitHub. You can run the unit tests to see the results of both proper and improper exception handling in action.

You may also be interested in

Pages: 1 2

No comments:

Post a Comment