April 5, 2015

6 rules of exception handling by example (part 1 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.

Error handling is a crucial part of any software application. If you don’t pay attention to it, if you don’t take any coordinated measures, your application will become instable, error-prone and hard to test and maintain. But on the other hand, if you do take action, but you do it the wrong way, it gets even worse.

In Java, the concept of the Exception is vital to error handling in the application. It’s a very common, safe, mature concept. It’s really a core concept of the language, thus covered in any basic course as well as in many books and online resources.

Still I’ve seen many developers struggling with some of the key ideas behind the concept of exception handling, thus undermining the aim of stabilizing and securing an application. That's why I’d like to publish yet another article on that topic which illustrates anti-patterns and best practices in an example-driven way.

The Six Rules

There are six rules any exception handling strategy should obey. According to my experience, exception handling fails whenever these rules get violated. They are:
  1. Never use a magical error code as the return value.
  2. Never EVER return null to signal an error occurrence.
  3. Never swallow an exception you cannot handle; throw it instead.
  4. Use unchecked exceptions for technical errors (you can’t handle them and recover); use checked exceptions sparingly for business error (you know how to handle them and recover).
  5. Use the most fine-grained exception subclass possible.
  6. Error handling is a cross-cutting concern. That’s what AOP frameworks are for; use them.
As with any rule in software development, they need justification unless negligible. I’ll illustrate my reasoning for every single one of those rules in the ensuing sections.

Magical error codes

The anti-pattern

Well, we all know this is wrong, right? Yet I’ve seen code going to production with error handling based on magical return codes. This drastically increases coupling between caller and callee, it violates the Separation of Concerns principle (application code vs. error handling) and diminishes maintainability. It breaks compile time checking, thus diminishing stability. Because pure return values can simply be left ignored, it also impedes error safety.

That’s why we always use exceptions to encapsulate errors and error handling. There is no justification for not doing so, and most developers will agree on that. However, I’ve seen code strangely interweaving outdated error-code thinking in exception-based error handling, like so:
public void callBusiness() {
    try {
        doBusiness();
    } catch (MagicalErrorCodeException ex) {
        switch (ex.reason) {
            case INPUT_EMPTY:
                DummyExceptionHandler.handleCriticalError(ex);
                break;
            case INPUT_NOT_NUMERICAL:
                DummyExceptionHandler.showMessage("Input must be numerical.");
                break;
            case INPUT_TOO_LONG:
                DummyExceptionHandler.showMessage("Input contains too many digits.");
                break;
            default:
                throw new IllegalArgumentException("Exception reason not supported: " + ex.reason);
        }
    }
}

where the exception class is defined as:
public class MagicalErrorCodeException extends Exception {
    public final Reason reason;
    
    public static enum Reason {
        INPUT_EMPTY,
        INPUT_TOO_LONG,
        INPUT_NOT_NUMERICAL,
    }

    public MagicalErrorCodeException(Reason reason) {
        this.reason = reason;
    }
}

Whilst magical error codes are typically of type int, really any type can be abused as a magical error code; here, it’s an enum type, which, however typesafe as opposed to int or String values, does not change the underlying problem of violating the open/closed principle. We have no idea what any particular Reason implies unless we scan the source code for its usages.

In this example, there is yet another software design anti-pattern: As both INPUT_NOT_NUMERICAL and INPUT_TOO_LONG enum values trigger the same error handling code, there’s a DRY violation.

In essence, error handling should not care about our exception class internals. Because in this anti-pattern example, our exception class is nothing but a dumb state container, the logic illegally resides in the error handling code which thus also violates Java exception handling design by implementing a custom “catch switch”.

The solution

For this example, the solution is twofold, eliminating (a) the DRY violation and (b) the open/closed principle violation. At the same time, we will apply proper Java exception handling design principles.

Here’s the fixed error handling:
public void callBusiness() {
    try {
        doBusiness();
    } catch (CriticalException ex) {
        DummyExceptionHandler.handleCriticalError(ex);
    } catch (MagicalErrorCodeExceptionFixed ex) {
        DummyExceptionHandler.showMessage(ex.getMessage());
    }
}

And here’s the fixed exception class:
public class MagicalErrorCodeExceptionFixed extends Exception {
    private final Reason reason;
    
    public static enum Reason {
        CRITICAL(null),
        INPUT_TOO_LONG("Input must be numerical."),
        INPUT_NOT_NUMERICAL("Input contains too many digits.");
        
        private final String message;

        private Reason(String message) {
            this.message = message;
        }
    }

    public MagicalErrorCodeExceptionFixed(Reason reason) {
        this.reason = reason;
    }

    @Override
    public String getMessage() {
        return reason.message;
    }
}

First, we concentrate on the error handling’s catch (MagicalErrorCodeExceptionFixed ex) branch which is now implemented in a DRY-compliant manner. The Reason enum now knows the business value (here it’s a display message) associated with it, and the exception class applies the state pattern to return that value through the standard getMessage() method.

Let’s assume that the former INPUT_EMPTY Reason value marks a more severe exception case with must be handled individually. Here we use the standard Java way to distinct different kinds of exceptions: By implementing individual exception classes for each respective exception type. In the example, we implemented an additional exception class:
public class CriticalException extends MagicalErrorCodeExceptionFixed {
     public CriticalException() {
          super(Reason.CRITICAL);
     }
}

In the exception handling code, we can now make a clean distinction of the two exception types.

Note that in a strongly object oriented architecture, we could even implement exception handling using a strategy pattern on the exception class. As exception handling typically involves much context information, it is still often preferred to implement it in a more service-oriented way, just as we did here.

Returning null as an error signal

The anti-pattern

The second most severe exception handling design error is using null to signal an error occurrence. To make this very clear: You must never, ever use null to signal an error occurrence. Doing so nukes traceability. Actually, we can consider null to be abused here as just another magical error code, as we discussed them above. Yet using null is actually the worst of all error code choices as it can – by definition – literally provide no information about what went wrong. What’s more, it could even lead to side effect errors.

Let’s observe this example code:
public class NullReturnClass {
     private static final String FILE_PATH = "my/file/path.txt";
     
     private File tryLoadFile() {
          // let's assume file loading failed
          boolean fileLoadedSuccessfully = false;
          
          if (fileLoadedSuccessfully) {
                return new File(FILE_PATH);
          }
          else {
                return null;
          }
     }
     
     public String getFileName() {
          File file = tryLoadFile();
          return file.getName();
     }
}

Here, tryLoadFile() returns null in order to signal that the file has not been loaded properly. The invoking method, getFileName(), not recognizing this as a postcondition, will try to operate on the returning object which would immediately trigger a NulllPointerexception as a side effect. Note that in this case, any information about the actual error source (the fact that tryLoadFile() failed) is lost.

Things are even worse in a situation where you actually do not use the return value of a method that might return null. You will just miss any information about the error occurrence, potentially leaving the application in an undefined state.

The solution

Here is the fixed version of above implementation:
public class NullReturnClassFixed {
    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() {
        File file;
        try {
            file = tryLoadFile();
        } catch (FileNotFoundException ex) {
            throw new RuntimeException(ex);
        }
        return file.getName();
    }
}

Here, tryLoadFile will throw an exception if it runs into an erroneous state. (Because we are on a “file handling” abstraction layer, this is considered a “business error” here and thus using a checked exception, refer to the respective section later on for more details.)

When calling that method, we can properly handle the error condition, and we can be sure that any unchecked exceptions would be propagated. Here, traceability is kept. There are no side effects from failed method calls.

Swallowing exceptions

The anti-pattern

Swallowing an exception of course is bad as it means loss of information. It can actually occur as a side effect of many of the anti-patterns presented here (i.e. using magic / null return values, not using AOP). But it can also occur where apparently proper exception handling is used.

As an example, we will redesign the “file handling” class from the Returning null as an error signal section. This time, we want the client to handle file loading errors, so we propagate the FileNotFoundexception as a business exception (hence a checked exception, as explained in the next section in more detail). Here is the relevant method:
public String getFileName() throws SwallowingException {
    File file = null;
    try {
        file = tryLoadFile();
    } catch (FileNotFoundException ex) {
        throw new SwallowingException(FILE_PATH);
    }
    return file.getName();
}

Note that the “business error” class’s constructor (SwallowingException(String)) only takes a String as a parameter. Information about the root exception (FileNotFoundexception) is lost. Your IDE would probably even moan about the “ex” variable not being used.

The solution

If we design our own exception class which is used as a wrapper or as a “translator” of technical exceptions to business exceptions, we must be sure to always keep information about the original exception in the stacktrace. In order to do so, of course, we use the exception’s built-in stacking of causes by invoking one of the super constructors:
public class SwallowingExceptionFixed extends Exception {
    private final String filePath;

    public SwallowingExceptionFixed(String filePath, Throwable cause) {
        super(cause);
        this.filePath = filePath;
    }

    @Override
    public String getMessage() {
        return filePath + " could not be found.";
    }
}

Now all we need to do is provide the original exception when using our subclasses constructor, like so:
catch (FileNotFoundException ex) {
    throw new SwallowingExceptionFixed(FILE_PATH, ex);
}

Information about the original error is now preserved in the stacktrace.


Pages: 1 2

No comments:

Post a Comment