Thursday, August 3, 2017

Exception Enrichment in Java

Exception enrichment is an alternative to exception wrapping. Exception wrapping has a couple of disadvantages that exception enrichment can fix. These disadvantages are:
  • Exception wrapping may result in very long stack traces consisting of one stack trace for each exception in the wrapping hierarchy. Most often only the root stack trace is interesting. The rest of the stack traces are then just annoying.

  • The messages of the exceptions are spread out over the stack traces. The message of an exception is typically printed above the stack trace. When several exceptions wrap each other in a hierarchy, all these messages are spread out in between the stack traces. This makes it harder to determine what went wrong, and what the program was trying to do when the error happened. In other words, it makes it hard to determine in what context the error occurred. The error might have occurred in a PersonDao class, but was it called from a servlet or from a web service when it failed?

In exception enrichment you do not wrap exceptions. Instead you add contextual information to the original exception and rethrow it. Rethrowing an exception does not reset the stack trace embedded in the exception.
Here is an example:
  public void method2() throws EnrichableException{
     try{
        method1(); 
     } catch(EnrichableException e){
        e.addInfo("An error occurred when trying to ...");
        throw e;
     }
  }
  
  public void method1() throws EnrichableException {
     if(...) throw new EnrichableException(
        "Original error message");   
  }
As you can see the method1() throws an EnrichableException which is a superclass for enrichable exceptions. This is not a standard Java exception, so you will have to create it yourself. There is an example EnrichableException at the end of this text.
Notice how method2() calls the addInfo() method on the caught EnrichableException, and rethrow it afterwards. As the exception propagates up the call stack, each catch block can add relevant information to the exception if necessary.
Using this simple technique you only get a single stack trace, and still get any relevant contextual information necessary to investigate the cause of the exception.

Unique Error Codes

It is sometimes a requirement that each error raised in an application is identified by a unique error code. This can be a bit problematic since some errors are raised inside components that are reused throughout the application. Therefore an exception may seem the same to the component throwing it, but the context in which the exception occurs is different.
Here is an example:
  public void method3() throws EnrichableException{
     try{
        method1(); 
     } catch(EnrichableException e){
        e.addInfo("An error occurred when trying to ...");
        throw e;
     }
  }

  public void method2() throws EnrichableException{
     try{
        method1(); 
     } catch(EnrichableException e){
        e.addInfo("An error occurred when trying to ...");
        throw e;
     }
  }
  
  public void method1() throws EnrichableException {
     if(...) throw new EnrichableException(
        "ERROR1", "Original error message");   
  }
Notice how method1() adds the code "ERROR1" to the thrown EnrichableException to uniquely identify that error cause. But notice too that method1() is called from both method2() and method3(). Though the error may seem the same to method1() no matter which of method2() and method3() that called it, this may important to know for the developer investigating the error. The error code "ERROR1" is enough to determine where the error occurred, but not in what context it occurred.
A solution to this problem is to add unique context error codes to the exception the same way the other contextual information is added. Here is an example where the addInfo() method has been changed to accommodate this:
  public void method3() throws EnrichableException{
     try{
        method1(); 
     } catch(EnrichableException e){
        e.addInfo("METHOD3", "ERROR1",
            "An error occurred when trying to ...");
        throw e;
     }
  }

  public void method2() throws EnrichableException{
     try{
        method1(); 
     } catch(EnrichableException e){
        e.addInfo("METHOD2", "ERROR1",
            "An error occurred when trying to ...");
        throw e;
     }
  }
  
  public void method1() throws EnrichableException {
     if(...) throw new EnrichableException(
        "METHOD1", "ERROR1", "Original error message");   
  }
Two new parameters have been added to the addInfo() method and the constructor of the EnrichableException. The first parameter is a code identifying the context in which the error occurred. The second parameter is a unique error code within that context. An error identification for an exception thrown by method1() when called from method2() will now look like this:
 [METHOD2:ERROR1][METHOD1:ERROR1]
When method1() is called from method3() the error identification will look like this:
 [METHOD3:ERROR1][METHOD1:ERROR1]
As you can see it is now possible to distinguish an exception thrown from method1() via method2() from the same exception thrown from method1() via method3().
You may not always need the extra contextual error codes, but when you do the solution sketched in this section is an option.

Wrapping Non-Enrichable Exceptions

You may not always be able to avoid exception wrapping. If a component in your application throws a checked exception that is not enrichable, you may have to wrap it in an enrichable exception. Here is an example where method1() catches a non-enrichable exception and wraps it in an enrichable exception, and throws the enrichable exception:
  public void method1() throws EnrichableException {
    
     try{
    
        ... call some method that throws an IOException ...
    
     } catch(IOException ioexception)
        throw new EnrichableException( ioexception,
          "METHOD1", "ERROR1", "Original error message");   
  }

Unchecked EnrichableException

I used to be in favor of checked exceptions but over the last couple of years my opinion has changed. Now i feel that checked exceptions are more of a nuisance than a help. Therefore I would prefer to make my EnrichableException unchecked, by having it extend RuntimeException.
There is a more thorough discussion of checked and unchecked exceptions in the text "Checked vs. Unchecked Exceptions".

Exception Enrichment and Pluggable Exception Handlers

Like with any other exception type it is possible to use pluggable exception handlers with enrichable exceptions. If you use the unique error codes described earlier these codes must be added as paremeters to the exception handler interface. Here is an example exception handler interface supporting these unique error codes:
public interface ExceptionHandler{

   public void handle(String contextCode, String errorCode,
                      String errorText, Throwable t)

   public void raise(String contextCode, String errorCode,
                     String errorText);
   
}
Exceptions caught in the program will be passed to the handleException() which will decide what concrete exception to throw instead. In this case an EnrichableException is thrown. If the EnrichableException is unchecked it is not necessary to declare it in the handleException() method.

An Example EnrichableException

Below is an example of an enrichable exception that you can use as a template for your own enrichable exceptions. You may need to change the class definition to suit your own needs. The exception is designed to use unique error codes as described earlier in this text.
Below the code is an example application that uses the EnrichableException, and a stack trace generated from this application.
import java.util.ArrayList;
import java.util.List;

public class EnrichableException extends RuntimeException {
    public static final long serialVersionUID = -1;

    protected List<InfoItem> infoItems =
            new ArrayList<InfoItem>();

    protected class InfoItem{
        public String errorContext = null;
        public String errorCode  = null;
        public String errorText  = null;
        public InfoItem(String contextCode, String errorCode,
                                     String errorText){

            this.errorContext = contextCode;
            this.errorCode   = errorCode;
            this.errorText   = errorText;
        }
    }


    public EnrichableException(String errorContext, String errorCode,
                               String errorMessage){

        addInfo(errorContext, errorCode, errorMessage);
    }

    public EnrichableException(String errorContext, String errorCode,
                               String errorMessage, Throwable cause){
        super(cause);
        addInfo(errorContext, errorCode, errorMessage);
    }

    public EnrichableException addInfo(
        String errorContext, String errorCode, String errorText){

        this.infoItems.add(
            new InfoItem(errorContext, errorCode, errorText));
        return this;
    }

    public String getCode(){
        StringBuilder builder = new StringBuilder();

        for(int i = this.infoItems.size()-1 ; i >=0; i--){
            InfoItem info =
                this.infoItems.get(i);
            builder.append('[');
            builder.append(info.errorContext);
            builder.append(':');
            builder.append(info.errorCode);
            builder.append(']');
        }

        return builder.toString();
    }

    public String toString(){
        StringBuilder builder = new StringBuilder();

        builder.append(getCode());
        builder.append('\n');


        //append additional context information.
        for(int i = this.infoItems.size()-1 ; i >=0; i--){
            InfoItem info =
                this.infoItems.get(i);
            builder.append('[');
            builder.append(info.errorContext);
            builder.append(':');
            builder.append(info.errorCode);
            builder.append(']');
            builder.append(info.errorText);
            if(i>0) builder.append('\n');
        }

        //append root causes and text from this exception first.
        if(getMessage() != null) {
            builder.append('\n');
            if(getCause() == null){
                builder.append(getMessage());
            } else if(!getMessage().equals(getCause().toString())){
                builder.append(getMessage());
            }
        }
        appendException(builder, getCause());

        return builder.toString();
    }

    private void appendException(
            StringBuilder builder, Throwable throwable){
        if(throwable == null) return;
        appendException(builder, throwable.getCause());
        builder.append(throwable.toString());
        builder.append('\n');
    }

[L1:E1][L2:E2][L3:E3]
[L1:E1]Error in level 1, calling level 2
[L2:E2]Error in level 2, calling level 3
[L3:E3]Error at level 3
java.lang.IllegalArgumentException: incorrect argument passed

 at exception.ExceptionTest$1.handle(ExceptionTest.java:8)
 at exception.ExceptionTest.level3(ExceptionTest.java:49)
 at exception.ExceptionTest.level2(ExceptionTest.java:38)
 at exception.ExceptionTest.level1(ExceptionTest.java:29)
 at exception.ExceptionTest.main(ExceptionTest.java:21)
Caused by: java.lang.IllegalArgumentException: incorrect argument passed
 at exception.ExceptionTest.level4(ExceptionTest.java:54)
 at exception.ExceptionTest.level3(ExceptionTest.java:47)
 ... 3 more

public class ExceptionTest {
 
    protected ExceptionHandler exceptionHandler = new ExceptionHandler(){
        public void handle(String errorContext, String errorCode,
                           String errorText, Throwable t){

            if(! (t instanceof EnrichableException)){
                throw new EnrichableException(
                    errorContext, errorCode, errorText, t);
            } else {
                ((EnrichableException) t).addInfo(
                    errorContext, errorCode, errorText);
            }
        }

        public void raise(String errorContext, String errorCode,
                          String errorText){
            throw new EnrichableException(
                errorContext, errorCode, errorText);
        }
    };

    public static void main(String[] args){
        ExceptionTest test = new ExceptionTest();
        try{
            test.level1();
        } catch(Exception e){
            e.printStackTrace();
        }
    }

    public void level1(){
        try{
            level2();
        } catch (EnrichableException e){
            this.exceptionHandler.handle(
                "L1", "E1", "Error in level 1, calling level 2", e);
            throw e;
        }
    }

    public void level2(){
        try{
            level3();
        } catch (EnrichableException e){
            this.exceptionHandler.handle(
                "L2", "E2", "Error in level 2, calling level 3", e);
            throw e;
        }
    }

    public void level3(){
        try{
            level4();
        } catch(Exception e){
            this.exceptionHandler.handle(
                "L3", "E3", "Error at level 3", e);
        }
    }

    public void level4(){
        throw new IllegalArgumentException("incorrect argument passed");
    }

} 
 
@reference_1_tutorials.jenkov.com
Exception Enrichment in Java

No comments:

Post a Comment